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.
@@ -1,445 +1,429 @@
1
- import chalk from 'chalk';
2
- import ora from 'ora';
3
- import path from 'path';
4
- import {
5
- getCurrentGitHubUser,
6
- verifyAndFetchUser,
7
- listUserRepos,
8
- createGitHubRepo,
9
- initiateDeviceFlow,
10
- pollDeviceToken,
11
- openBrowser
12
- } from '../github.js';
13
- import {
14
- saveGitHubToken,
15
- removeGitHubToken,
16
- getAllGitHubAccounts,
17
- setActiveGitHubAccount
18
- } from '../config.js';
19
- import { selectWithBack, inputWithBack, selectBox, BACK } from '../prompt-helpers.js';
20
- import {
21
- printSection,
22
- printGitHubProfile,
23
- printDeviceCodePrompt,
24
- printRepoCreatedBox,
25
- printErrorBox,
26
- printCancelled
27
- } from '../ui.js';
28
-
29
- /**
30
- * Hiển thị thông tin tài khoản GitHub hiện tại
31
- */
32
- export async function showGitHubStatus() {
33
- const spinner = ora({ text: 'Đang kiểm tra kết nối GitHub…', color: 'cyan' }).start();
34
- const info = await getCurrentGitHubUser();
35
- spinner.stop();
36
-
37
- if (!info.authenticated) {
38
- printSection('TRẠNG THÁI GITHUB');
39
- console.log(chalk.hex('#F59E0B')(' [!] Chưa kết nối tài khoản GitHub.'));
40
- console.log(chalk.hex('#94A3B8')(' Chạy "git-push-time github login" để kết nối tài khoản qua trình duyệt.\n'));
41
- return false;
42
- }
43
-
44
- printGitHubProfile(info.user, info.source);
45
- return true;
46
- }
47
-
48
- /**
49
- * Đăng nhập GitHub qua OAuth Device Flow
50
- */
51
- async function loginViaDeviceFlow() {
52
- const initSpinner = ora({ text: 'Đang kết nối GitHub OAuth…', color: 'cyan' }).start();
53
- const init = await initiateDeviceFlow();
54
- initSpinner.stop();
55
-
56
- if (!init.ok || !init.data) {
57
- printErrorBox('KẾT NỐI OAUTH THẤT BẠI', [init.error || 'Không nhận được mã xác thực từ GitHub.']);
58
- return null;
59
- }
60
-
61
- const { device_code, user_code, verification_uri, interval, expires_in } = init.data;
62
-
63
- // Hiển thị box mã xác thực cho user
64
- printDeviceCodePrompt({ userCode: user_code, verificationUri: verification_uri });
65
-
66
- // Tự động mở browser
67
- openBrowser(verification_uri);
68
-
69
- const pollSpinner = ora({
70
- text: chalk.hex('#38BDF8')('Đang chờ bạn xác nhận và nhấn [Authorize] trên trình duyệt…'),
71
- color: 'cyan'
72
- }).start();
73
-
74
- const pollResult = await pollDeviceToken(device_code, interval, expires_in, (status) => {
75
- if (status === 'slow_down') {
76
- pollSpinner.text = chalk.hex('#F59E0B')('GitHub yêu cầu giãn cách kết nối, đang tiếp tục chờ bạn xác nhận…');
77
- }
78
- });
79
-
80
- if (!pollResult.ok || !pollResult.token) {
81
- pollSpinner.fail(chalk.red('Xác thực thất bại!'));
82
- printErrorBox('ĐĂNG NHẬP THẤT BẠI', [pollResult.error]);
83
- return null;
84
- }
85
-
86
- pollSpinner.text = 'Đang tải thông tin tài khoản…';
87
- const userRes = await verifyAndFetchUser(pollResult.token);
88
- if (!userRes.ok) {
89
- pollSpinner.fail(chalk.red('Không thể tải profile GitHub!'));
90
- printErrorBox('LỖI PROFILE', [userRes.error]);
91
- return null;
92
- }
93
-
94
- saveGitHubToken(pollResult.token, userRes.user);
95
- pollSpinner.succeed(chalk.hex('#34D399').bold(`Đăng nhập thành công! Xin chào @${userRes.user.login}`));
96
- printGitHubProfile(userRes.user, 'oauth-device');
97
- return userRes.user;
98
- }
99
-
100
- /**
101
- * Đăng nhập GitHub bằng Personal Access Token (PAT)
102
- */
103
- async function loginViaToken() {
104
- printSection('ĐĂNG NHẬP BẰNG PERSONAL ACCESS TOKEN');
105
- console.log(chalk.hex('#38BDF8')(' » Hướng dẫn lấy mã Token:'));
106
- console.log(chalk.hex('#94A3B8')(' 1. Mở: https://github.com/settings/tokens'));
107
- console.log(chalk.hex('#94A3B8')(' 2. Chọn "Generate new token (classic)"'));
108
- console.log(chalk.hex('#94A3B8')(' 3. Tích chọn quyền: ') + chalk.hex('#34D399').bold('repo') + chalk.hex('#94A3B8')(' (Full control of repositories)'));
109
- console.log(chalk.hex('#94A3B8')(' 4. Copy mã token (bắt đầu bằng ghp_ hoặc github_pat_) và dán vào bên dưới.\n'));
110
-
111
- const tokenInput = await inputWithBack({
112
- message: 'GitHub Token (PAT)',
113
- hint: ['Bắt đầu bằng ghp_ hoặc github_pat_ · cần quyền repo'],
114
- validate: (val) => {
115
- if (!val || !val.trim()) return 'Vui lòng không để trống token!';
116
- if (val.trim().length < 15) return 'Mã token dường như quá ngắn. Vui lòng kiểm tra lại.';
117
- return true;
118
- }
119
- });
120
-
121
- if (tokenInput === BACK) return null;
122
-
123
- const spinner = ora({ text: 'Đang xác thực token với GitHub…', color: 'cyan' }).start();
124
- const res = await verifyAndFetchUser(tokenInput.trim());
125
- if (!res.ok) {
126
- spinner.fail(chalk.red('Xác thực thất bại!'));
127
- printErrorBox('TOKEN KHÔNG HỢP LỆ', [res.error], 'Kiểm tra token còn hạn và có quyền "repo" không.');
128
- return null;
129
- }
130
-
131
- saveGitHubToken(tokenInput.trim(), res.user);
132
- spinner.succeed(chalk.hex('#34D399')(`Đăng nhập thành công! Xin chào @${res.user.login}`));
133
- printGitHubProfile(res.user, 'config');
134
- return res.user;
135
- }
136
-
137
- /**
138
- * Menu đăng nhập GitHub linh hoạt (OAuth Device Flow hoặc Token)
139
- */
140
- export async function promptGitHubLogin() {
141
- printSection('ĐĂNG NHẬP TÀI KHOẢN GITHUB');
142
-
143
- const method = await selectWithBack({
144
- message: 'Chọn phương thức đăng nhập:',
145
- choices: [
146
- {
147
- name: '[OAuth] Mở trình duyệt xác nhận (Device Flow) — Khuyên dùng',
148
- value: 'oauth',
149
- description: 'Tự động mở trình duyệt, nhập mã xác thực không cần copy token'
150
- },
151
- {
152
- name: '[Token] Dán Personal Access Token (PAT)',
153
- value: 'token',
154
- description: 'Nhập token bảo mật tự tạo trên github.com/settings/tokens'
155
- }
156
- ]
157
- });
158
-
159
- if (method === BACK) return null;
160
- if (method === 'oauth') return await loginViaDeviceFlow();
161
- if (method === 'token') return await loginViaToken();
162
- return null;
163
- }
164
-
165
- /**
166
- * Bước chọn GitHub Account trong luồng Wizard Push
167
- * @returns {Promise<{ useGitHub: boolean, user?: any, back?: boolean }>}
168
- */
169
- export async function promptSelectGitHubAccount({ clearPromptOnDone = false } = {}) {
170
- if (!clearPromptOnDone) printSection('CHỌN TÀI KHOẢN GITHUB');
171
-
172
- const accounts = getAllGitHubAccounts();
173
- const currentStatus = await getCurrentGitHubUser();
174
-
175
- if (accounts.length === 0 && !currentStatus.authenticated) {
176
- // Chưa có tài khoản nào
177
- const loginChoice = await selectWithBack({
178
- message: 'Bạn chưa kết nối tài khoản GitHub. Bạn muốn làm gì?',
179
- clearPromptOnDone,
180
- choices: [
181
- {
182
- name: 'Đăng nhập GitHub ngay (Mở trình duyệt xác thực OAuth)',
183
- value: 'login',
184
- description: 'Kết nối tài khoản để chọn/tạo repo tự động'
185
- },
186
- {
187
- name: 'Bỏ qua GitHub (Dán URL Git repository thủ công)',
188
- value: 'skip',
189
- description: 'Không dùng tài khoản GitHub, nhập link https://... trực tiếp'
190
- }
191
- ]
192
- });
193
-
194
- if (loginChoice === BACK) return { back: true };
195
- if (loginChoice === 'login') {
196
- const user = await promptGitHubLogin();
197
- if (user) {
198
- return { useGitHub: true, user };
199
- }
200
- // Nếu đăng nhập hủy/thất bại, hỏi có muốn nhập URL thủ công không
201
- return { useGitHub: false };
202
- }
203
- return { useGitHub: false };
204
- }
205
-
206
- // Đã có tài khoản
207
- const currentLogin = currentStatus.authenticated ? currentStatus.user.login : accounts[0]?.login;
208
- const choices = [
209
- {
210
- name: `Tiếp tục với @${currentLogin} (Đang chọn)`,
211
- value: `use:${currentLogin}`,
212
- description: `Dùng tài khoản @${currentLogin} để liên kết repository`
213
- }
214
- ];
215
-
216
- // Danh sách các tài khoản khác
217
- for (const acc of accounts) {
218
- if (acc.login !== currentLogin) {
219
- choices.push({
220
- name: `Đổi sang @${acc.login}`,
221
- value: `switch:${acc.login}`,
222
- description: `Chuyển sang tài khoản @${acc.login}`
223
- });
224
- }
225
- }
226
-
227
- choices.push(
228
- {
229
- name: '+ Đăng nhập thêm tài khoản GitHub khác',
230
- value: 'add_account',
231
- description: 'Đăng nhập tài khoản mới bằng OAuth hoặc Token'
232
- },
233
- {
234
- name: 'Bỏ qua GitHub (Dán URL repository thủ công)',
235
- value: 'skip',
236
- description: 'Nhập link repo bằng tay'
237
- }
238
- );
239
-
240
- const picked = await selectWithBack({
241
- message: 'Chọn tài khoản GitHub sẽ sử dụng:',
242
- clearPromptOnDone,
243
- choices
244
- });
245
-
246
- if (picked === BACK) return { back: true };
247
- if (picked === 'skip') return { useGitHub: false };
248
-
249
- if (picked === 'add_account') {
250
- const newUser = await promptGitHubLogin();
251
- if (newUser) return { useGitHub: true, user: newUser };
252
- return { useGitHub: true, user: currentStatus.user };
253
- }
254
-
255
- if (picked.startsWith('switch:')) {
256
- const targetLogin = picked.replace('switch:', '');
257
- setActiveGitHubAccount(targetLogin);
258
- const updated = await getCurrentGitHubUser();
259
- console.log(chalk.hex('#34D399')(` [OK] Đã chuyển sang tài khoản @${targetLogin}\n`));
260
- return { useGitHub: true, user: updated.user };
261
- }
262
-
263
- return { useGitHub: true, user: currentStatus.user };
264
- }
265
-
266
- /**
267
- * Luồng tạo repository mới trên GitHub
268
- * @param {object} options
269
- * @param {string} options.targetDir Thư mục nguồn (dùng để gợi ý tên repo mặc định)
270
- * @returns {Promise<{ ok: boolean, repo?: any, action?: 'push' | 'done', back?: boolean }>}
271
- */
272
- export async function promptCreateRepo({ targetDir = process.cwd() } = {}) {
273
- let currentUser = await getCurrentGitHubUser();
274
- if (!currentUser.authenticated) {
275
- console.log(chalk.hex('#F59E0B')('\n [!] Bạn cần kết nối tài khoản GitHub trước khi tạo repository.'));
276
- const loginUser = await promptGitHubLogin();
277
- if (!loginUser) {
278
- return { ok: false, back: true };
279
- }
280
- currentUser = { authenticated: true, user: loginUser };
281
- }
282
-
283
- printSection('TẠO REPOSITORY MỚI TRÊN GITHUB');
284
- console.log(chalk.hex('#94A3B8')(` » Đang tạo trên tài khoản: ${chalk.hex('#38BDF8').bold('@' + currentUser.user.login)}`));
285
-
286
- const folderName = path.basename(path.resolve(targetDir))
287
- .toLowerCase()
288
- .replace(/[^a-z0-9_.-]/g, '-');
289
-
290
- const repoName = await inputWithBack({
291
- message: 'Tên repository mới',
292
- hint: ['Chỉ chữ, số, -, _ , . (VD: my-awesome-repo)'],
293
- default: folderName || 'my-awesome-repo',
294
- validate: (val) => {
295
- const trimmed = (val || '').trim();
296
- if (!trimmed) return 'Tên repo không được để trống!';
297
- if (!/^[a-zA-Z0-9_.-]+$/.test(trimmed)) {
298
- return 'Tên repo chỉ chứa chữ, số, dấu gạch -, gạch dưới _ hoặc dấu chấm .';
299
- }
300
- return true;
301
- }
302
- });
303
-
304
- if (repoName === BACK) return { ok: false, back: true };
305
-
306
- const description = await inputWithBack({
307
- message: 'Mô tả ngắn cho dự án',
308
- hint: ['Tùy chọn, Enter để bỏ qua'],
309
- default: ''
310
- });
311
- if (description === BACK) return { ok: false, back: true };
312
-
313
- const visibility = await selectWithBack({
314
- message: 'Quyền riêng tư của repository:',
315
- choices: [
316
- { name: 'Public (Bất kỳ ai cũng có thể xem)', value: 'public', description: 'Mã nguồn mở công khai' },
317
- { name: 'Private (Chỉ bạn và người được phân quyền xem)', value: 'private', description: 'Kho lưu trữ riêng tư' }
318
- ]
319
- });
320
- if (visibility === BACK) return { ok: false, back: true };
321
-
322
- const isPrivate = visibility === 'private';
323
-
324
- const spinner = ora({ text: `Đang tạo repository '${repoName}' trên GitHub…`, color: 'cyan' }).start();
325
- const createRes = await createGitHubRepo({
326
- name: repoName,
327
- description: String(description || ''),
328
- isPrivate,
329
- autoInit: false
330
- });
331
-
332
- if (!createRes.ok) {
333
- spinner.fail(chalk.red('Không thể tạo repository!'));
334
- printErrorBox('TẠO REPO THẤT BẠI', [createRes.error]);
335
- return { ok: false, error: createRes.error };
336
- }
337
-
338
- spinner.succeed(chalk.hex('#34D399')(`Đã tạo repo @${currentUser.user.login}/${repoName} thành công!`));
339
- printRepoCreatedBox(createRes.repo);
340
-
341
- return { ok: true, repo: createRes.repo };
342
- }
343
-
344
- /**
345
- * Hiển thị danh sách các repo gần nhất của user
346
- */
347
- export async function showUserRepos({ limit = 15 } = {}) {
348
- const spinner = ora({ text: 'Đang tải danh sách repository từ GitHub…', color: 'cyan' }).start();
349
- const res = await listUserRepos({ limit });
350
- spinner.stop();
351
-
352
- if (!res.ok) {
353
- printErrorBox('KHÔNG TẢI ĐƯỢC DANH SÁCH REPO', [res.error]);
354
- return [];
355
- }
356
-
357
- if (res.repos.length === 0) {
358
- console.log(chalk.hex('#94A3B8')(' » Bạn chưa có repository nào trên tài khoản này.\n'));
359
- return [];
360
- }
361
-
362
- printSection(`DANH SÁCH REPOSITORIES (${res.repos.length} REPO GẦN ĐÂY)`);
363
- res.repos.forEach((r, idx) => {
364
- const lock = r.private ? chalk.hex('#F59E0B')('[Private]') : chalk.hex('#10B981')('[Public ]');
365
- const num = chalk.hex('#64748B')(String(idx + 1).padStart(2, ' '));
366
- const name = chalk.hex('#F8FAFC').bold(r.name);
367
- const url = chalk.hex('#38BDF8')(r.clone_url);
368
- console.log(` ${num}. ${lock} ${name}`);
369
- console.log(` ${chalk.hex('#64748B')('URL:')} ${url}`);
370
- });
371
- console.log('');
372
- return res.repos;
373
- }
374
-
375
- /**
376
- * Đăng xuất tài khoản GitHub (mặc định: tài khoản đang active)
377
- */
378
- export function logoutGitHub(login = null) {
379
- const accounts = getAllGitHubAccounts();
380
- const target = login || accounts.find((a) => a.active)?.login || accounts[0]?.login || 'GitHub';
381
- removeGitHubToken(login);
382
- console.log(chalk.hex('#34D399')(`\n [OK] Đã đăng xuất @${target} và xóa token khỏi máy.\n`));
383
- }
384
-
385
- /**
386
- * Menu tương tác quản lý GitHub
387
- */
388
- export async function runGitHubMenu({ defaultDir = process.cwd() } = {}) {
389
- while (true) {
390
- const userStatus = await getCurrentGitHubUser();
391
- const isLogged = userStatus.authenticated;
392
-
393
- printSection('QUẢN LÝ GITHUB & REPOSITORY');
394
- if (isLogged) {
395
- console.log(chalk.hex('#34D399')(` [✓] Đang đăng nhập: @${userStatus.user.login} (${userStatus.user.name || 'Developer'})`));
396
- } else {
397
- console.log(chalk.hex('#F59E0B')(' [!] Chưa kết nối GitHub.'));
398
- }
399
-
400
- const choices = [
401
- { name: 'Tạo repository', value: 'create', description: 'Tạo repo mới và có thể đẩy code ngay' },
402
- { name: 'Tài khoản GitHub', value: 'status', description: 'Xem profile và trạng thái kết nối' },
403
- { name: 'Danh sách repository', value: 'list', description: 'Duyệt repo cá nhân và URL clone' },
404
- isLogged
405
- ? { name: 'Đăng nhập tài khoản khác', value: 'login', description: 'Thêm hoặc chuyển tài khoản GitHub' }
406
- : { name: 'Đăng nhập', value: 'login', description: 'Kết nối bằng trình duyệt hoặc token' },
407
- ...(isLogged ? [{ name: 'Đăng xuất', value: 'logout', description: 'Xóa kết nối tài khoản đang dùng' }] : []),
408
- { name: 'Quay lại', value: BACK, description: 'Về bảng điều khiển' }
409
- ];
410
-
411
- const action = await selectBox({
412
- message: 'Chọn tác vụ GitHub:',
413
- hint: 'Tạo repo · xem profile · đổi tài khoản',
414
- choices
415
- });
416
-
417
- if (action === BACK) return;
418
-
419
- if (action === 'status') {
420
- await showGitHubStatus();
421
- } else if (action === 'login') {
422
- await promptGitHubLogin();
423
- } else if (action === 'create') {
424
- const res = await promptCreateRepo({ targetDir: defaultDir });
425
- if (res.ok && res.repo) {
426
- const askPush = await selectBox({
427
- message: 'Đẩy dự án hiện tại lên repo mới ngay?',
428
- hint: 'Tự động gán origin và chuyển sang màn hình push',
429
- choices: [
430
- { name: 'Có, đẩy dự án lên repo này ngay lập tức', value: 'push', description: 'Tự động gán origin và chuyển sang màn hình push' },
431
- { name: 'Không, chỉ tạo repo thôi', value: 'skip', description: 'Hoàn tất' }
432
- ]
433
- });
434
- if (askPush === 'push') {
435
- return { autoPush: true, repoUrl: res.repo.cloneUrl };
436
- }
437
- }
438
- } else if (action === 'list') {
439
- await showUserRepos();
440
- } else if (action === 'logout') {
441
- logoutGitHub();
442
- }
443
- }
444
- }
445
-
1
+ import chalk from 'chalk';
2
+ import ora from 'ora';
3
+ import path from 'path';
4
+ import {
5
+ getCurrentGitHubUser,
6
+ verifyAndFetchUser,
7
+ listUserRepos,
8
+ createGitHubRepo,
9
+ initiateDeviceFlow,
10
+ pollDeviceToken,
11
+ openBrowser
12
+ } from '../github.js';
13
+ import {
14
+ saveGitHubToken,
15
+ removeGitHubToken,
16
+ getAllGitHubAccounts,
17
+ setActiveGitHubAccount
18
+ } from '../config.js';
19
+ import { selectWithBack, inputWithBack, selectBox, BACK } from '../prompt-helpers.js';
20
+ import {
21
+ printSection,
22
+ printGitHubProfile,
23
+ printDeviceCodePrompt,
24
+ printRepoCreatedBox,
25
+ printErrorBox,
26
+ printCancelled
27
+ } from '../ui.js';
28
+
29
+ /**
30
+ * Display the currently connected GitHub account.
31
+ */
32
+ export async function showGitHubStatus() {
33
+ const spinner = ora({ text: 'Checking the GitHub connection…', color: 'cyan' }).start();
34
+ const info = await getCurrentGitHubUser();
35
+ spinner.stop();
36
+
37
+ if (!info.authenticated) {
38
+ printSection('GITHUB STATUS');
39
+ console.log(chalk.hex('#F59E0B')(' [!] No GitHub account is connected.'));
40
+ console.log(chalk.hex('#94A3B8')(' Run "git-push-time github login" to connect an account in your browser.\n'));
41
+ return false;
42
+ }
43
+
44
+ printGitHubProfile(info.user, info.source);
45
+ return true;
46
+ }
47
+
48
+ /**
49
+ * Sign in to GitHub with OAuth Device Flow.
50
+ */
51
+ async function loginViaDeviceFlow() {
52
+ const initSpinner = ora({ text: 'Connecting to GitHub OAuth…', color: 'cyan' }).start();
53
+ const init = await initiateDeviceFlow();
54
+ initSpinner.stop();
55
+
56
+ if (!init.ok || !init.data) {
57
+ printErrorBox('OAUTH CONNECTION FAILED', [init.error || 'GitHub did not return a verification code.']);
58
+ return null;
59
+ }
60
+
61
+ const { device_code, user_code, verification_uri, interval, expires_in } = init.data;
62
+
63
+ // Show the verification code to the user.
64
+ printDeviceCodePrompt({ userCode: user_code, verificationUri: verification_uri });
65
+
66
+ // Open the browser automatically.
67
+ openBrowser(verification_uri);
68
+
69
+ const pollSpinner = ora({
70
+ text: chalk.hex('#38BDF8')('Waiting for you to authorize access in your browser…'),
71
+ color: 'cyan'
72
+ }).start();
73
+
74
+ const pollResult = await pollDeviceToken(device_code, interval, expires_in, (status) => {
75
+ if (status === 'slow_down') {
76
+ pollSpinner.text = chalk.hex('#F59E0B')('GitHub requested a longer polling interval. Still waiting for authorization…');
77
+ }
78
+ });
79
+
80
+ if (!pollResult.ok || !pollResult.token) {
81
+ pollSpinner.fail(chalk.red('Authorization failed.'));
82
+ printErrorBox('SIGN-IN FAILED', [pollResult.error]);
83
+ return null;
84
+ }
85
+
86
+ pollSpinner.text = 'Loading account information…';
87
+ const userRes = await verifyAndFetchUser(pollResult.token);
88
+ if (!userRes.ok) {
89
+ pollSpinner.fail(chalk.red('Could not load the GitHub profile.'));
90
+ printErrorBox('PROFILE ERROR', [userRes.error]);
91
+ return null;
92
+ }
93
+
94
+ saveGitHubToken(pollResult.token, userRes.user);
95
+ pollSpinner.succeed(chalk.hex('#34D399').bold(`Signed in successfully. Welcome, @${userRes.user.login}.`));
96
+ printGitHubProfile(userRes.user, 'oauth-device');
97
+ return userRes.user;
98
+ }
99
+
100
+ /**
101
+ * Sign in to GitHub with a Personal Access Token (PAT).
102
+ */
103
+ async function loginViaToken() {
104
+ printSection('SIGN IN WITH A PERSONAL ACCESS TOKEN');
105
+ console.log(chalk.hex('#38BDF8')(' » To create a token:'));
106
+ console.log(chalk.hex('#94A3B8')(' 1. Open: https://github.com/settings/tokens'));
107
+ console.log(chalk.hex('#94A3B8')(' 2. Select "Generate new token (classic)"'));
108
+ console.log(chalk.hex('#94A3B8')(' 3. Grant the ') + chalk.hex('#34D399').bold('repo') + chalk.hex('#94A3B8')(' scope (full control of repositories)'));
109
+ console.log(chalk.hex('#94A3B8')(' 4. Copy the token (starts with ghp_ or github_pat_) and paste it below.\n'));
110
+
111
+ const tokenInput = await inputWithBack({
112
+ message: 'GitHub token (PAT)',
113
+ hint: ['Starts with ghp_ or github_pat_ · requires the repo scope'],
114
+ validate: (val) => {
115
+ if (!val || !val.trim()) return 'Token cannot be empty.';
116
+ if (val.trim().length < 15) return 'This token seems too short. Check it and try again.';
117
+ return true;
118
+ }
119
+ });
120
+
121
+ if (tokenInput === BACK) return null;
122
+
123
+ const spinner = ora({ text: 'Verifying token with GitHub…', color: 'cyan' }).start();
124
+ const res = await verifyAndFetchUser(tokenInput.trim());
125
+ if (!res.ok) {
126
+ spinner.fail(chalk.red('Verification failed.'));
127
+ printErrorBox('INVALID TOKEN', [res.error], 'Check that the token is active and has the "repo" scope.');
128
+ return null;
129
+ }
130
+
131
+ saveGitHubToken(tokenInput.trim(), res.user);
132
+ spinner.succeed(chalk.hex('#34D399')(`Signed in successfully. Welcome, @${res.user.login}.`));
133
+ printGitHubProfile(res.user, 'config');
134
+ return res.user;
135
+ }
136
+
137
+ /**
138
+ * Flexible GitHub sign-in menu (OAuth Device Flow or token).
139
+ */
140
+ export async function promptGitHubLogin() {
141
+ printSection('SIGN IN TO GITHUB');
142
+
143
+ const method = await selectWithBack({
144
+ message: 'Choose a sign-in method:',
145
+ choices: [
146
+ {
147
+ name: '[OAuth] Authorize in your browser (Device Flow) — Recommended',
148
+ value: 'oauth',
149
+ description: 'Open the browser and enter a verification code; no token copy is needed'
150
+ },
151
+ {
152
+ name: '[Token] Paste a Personal Access Token (PAT)',
153
+ value: 'token',
154
+ description: 'Enter a token created at github.com/settings/tokens'
155
+ }
156
+ ]
157
+ });
158
+
159
+ if (method === BACK) return null;
160
+ if (method === 'oauth') return await loginViaDeviceFlow();
161
+ if (method === 'token') return await loginViaToken();
162
+ return null;
163
+ }
164
+
165
+ /**
166
+ * GitHub account selection step in the push wizard.
167
+ * @returns {Promise<{ useGitHub: boolean, user?: any, back?: boolean }>}
168
+ */
169
+ export async function promptSelectGitHubAccount({ clearPromptOnDone = false } = {}) {
170
+ if (!clearPromptOnDone) printSection('CHOOSE A GITHUB ACCOUNT');
171
+
172
+ const accounts = getAllGitHubAccounts();
173
+ const currentStatus = await getCurrentGitHubUser();
174
+
175
+ if (accounts.length === 0 && !currentStatus.authenticated) {
176
+ // No account is connected.
177
+ const loginChoice = await selectWithBack({
178
+ message: 'No GitHub account is connected. What would you like to do?',
179
+ clearPromptOnDone,
180
+ choices: [
181
+ {
182
+ name: 'Sign in to GitHub (authorize in your browser)',
183
+ value: 'login',
184
+ description: 'Connect an account to choose or create repositories'
185
+ },
186
+ {
187
+ name: 'Skip GitHub (enter a repository URL manually)',
188
+ value: 'skip',
189
+ description: 'Continue without a GitHub account and enter an HTTPS URL'
190
+ }
191
+ ]
192
+ });
193
+
194
+ if (loginChoice === BACK) return { back: true };
195
+ if (loginChoice === 'login') {
196
+ const user = await promptGitHubLogin();
197
+ if (user) {
198
+ return { useGitHub: true, user };
199
+ }
200
+ // If sign-in is canceled or fails, allow the user to enter a URL manually.
201
+ return { useGitHub: false };
202
+ }
203
+ return { useGitHub: false };
204
+ }
205
+
206
+ // An account is already connected.
207
+ const currentLogin = currentStatus.authenticated ? currentStatus.user.login : accounts[0]?.login;
208
+ const choices = [
209
+ {
210
+ name: `Continue as @${currentLogin} (Current account)`,
211
+ value: `use:${currentLogin}`,
212
+ description: `Use @${currentLogin} to connect a repository`
213
+ }
214
+ ];
215
+
216
+ // List the other accounts.
217
+ for (const acc of accounts) {
218
+ if (acc.login !== currentLogin) {
219
+ choices.push({
220
+ name: `Switch to @${acc.login}`,
221
+ value: `switch:${acc.login}`,
222
+ description: `Use the @${acc.login} account`
223
+ });
224
+ }
225
+ }
226
+
227
+ choices.push(
228
+ {
229
+ name: '+ Sign in to another GitHub account',
230
+ value: 'add_account',
231
+ description: 'Add an account using OAuth or a token'
232
+ },
233
+ {
234
+ name: 'Skip GitHub (enter a repository URL manually)',
235
+ value: 'skip',
236
+ description: 'Enter a repository URL manually'
237
+ }
238
+ );
239
+
240
+ const picked = await selectWithBack({
241
+ message: 'Choose the GitHub account to use:',
242
+ clearPromptOnDone,
243
+ choices
244
+ });
245
+
246
+ if (picked === BACK) return { back: true };
247
+ if (picked === 'skip') return { useGitHub: false };
248
+
249
+ if (picked === 'add_account') {
250
+ const newUser = await promptGitHubLogin();
251
+ if (newUser) return { useGitHub: true, user: newUser };
252
+ return { useGitHub: true, user: currentStatus.user };
253
+ }
254
+
255
+ if (picked.startsWith('switch:')) {
256
+ const targetLogin = picked.replace('switch:', '');
257
+ setActiveGitHubAccount(targetLogin);
258
+ const updated = await getCurrentGitHubUser();
259
+ console.log(chalk.hex('#34D399')(` [OK] Switched to @${targetLogin}.\n`));
260
+ return { useGitHub: true, user: updated.user };
261
+ }
262
+
263
+ return { useGitHub: true, user: currentStatus.user };
264
+ }
265
+
266
+ /**
267
+ * Create a new GitHub repository.
268
+ * @param {object} options
269
+ * @param {string} options.targetDir Source folder (used to suggest a default repository name)
270
+ * @returns {Promise<{ ok: boolean, repo?: any, action?: 'push' | 'done', back?: boolean }>}
271
+ */
272
+ export async function promptCreateRepo({ targetDir = process.cwd() } = {}) {
273
+ let currentUser = await getCurrentGitHubUser();
274
+ if (!currentUser.authenticated) {
275
+ console.log(chalk.hex('#F59E0B')('\n [!] Connect a GitHub account before creating a repository.'));
276
+ const loginUser = await promptGitHubLogin();
277
+ if (!loginUser) {
278
+ return { ok: false, back: true };
279
+ }
280
+ currentUser = { authenticated: true, user: loginUser };
281
+ }
282
+
283
+ printSection('CREATE A GITHUB REPOSITORY');
284
+ console.log(chalk.hex('#94A3B8')(` » Creating under account: ${chalk.hex('#38BDF8').bold('@' + currentUser.user.login)}`));
285
+
286
+ const folderName = path.basename(path.resolve(targetDir))
287
+ .toLowerCase()
288
+ .replace(/[^a-z0-9_.-]/g, '-');
289
+
290
+ const repoName = await inputWithBack({
291
+ message: 'New repository name',
292
+ hint: ['Letters, numbers, -, _, and . only (e.g. my-awesome-repo)'],
293
+ default: folderName || 'my-awesome-repo',
294
+ validate: (val) => {
295
+ const trimmed = (val || '').trim();
296
+ if (!trimmed) return 'Repository name cannot be empty.';
297
+ if (!/^[a-zA-Z0-9_.-]+$/.test(trimmed)) {
298
+ return 'Use only letters, numbers, hyphens (-), underscores (_), or periods (.).';
299
+ }
300
+ return true;
301
+ }
302
+ });
303
+
304
+ if (repoName === BACK) return { ok: false, back: true };
305
+
306
+ const description = await inputWithBack({
307
+ message: 'Short project description',
308
+ hint: ['Optional; press Enter to skip'],
309
+ default: ''
310
+ });
311
+ if (description === BACK) return { ok: false, back: true };
312
+
313
+ const visibility = await selectWithBack({
314
+ message: 'Repository visibility:',
315
+ choices: [
316
+ { name: 'Public (Anyone can view it)', value: 'public', description: 'Public repository' },
317
+ { name: 'Private (Only you and collaborators can view it)', value: 'private', description: 'Private repository' }
318
+ ]
319
+ });
320
+ if (visibility === BACK) return { ok: false, back: true };
321
+
322
+ const isPrivate = visibility === 'private';
323
+
324
+ const spinner = ora({ text: `Creating '${repoName}' on GitHub…`, color: 'cyan' }).start();
325
+ const createRes = await createGitHubRepo({
326
+ name: repoName,
327
+ description: String(description || ''),
328
+ isPrivate,
329
+ autoInit: false
330
+ });
331
+
332
+ if (!createRes.ok) {
333
+ spinner.fail(chalk.red('Could not create repository.'));
334
+ printErrorBox('REPOSITORY CREATION FAILED', [createRes.error]);
335
+ return { ok: false, error: createRes.error };
336
+ }
337
+
338
+ spinner.succeed(chalk.hex('#34D399')(`Created @${currentUser.user.login}/${repoName} successfully.`));
339
+ printRepoCreatedBox(createRes.repo);
340
+
341
+ return { ok: true, repo: createRes.repo };
342
+ }
343
+
344
+ /**
345
+ * Display the user's most recently updated repositories.
346
+ */
347
+ export async function showUserRepos({ limit = 15 } = {}) {
348
+ const spinner = ora({ text: 'Loading repositories from GitHub…', color: 'cyan' }).start();
349
+ const res = await listUserRepos({ limit });
350
+ spinner.stop();
351
+
352
+ if (!res.ok) {
353
+ printErrorBox('COULD NOT LOAD REPOSITORIES', [res.error]);
354
+ return [];
355
+ }
356
+
357
+ if (res.repos.length === 0) {
358
+ console.log(chalk.hex('#94A3B8')(' » This account does not have any repositories yet.\n'));
359
+ return [];
360
+ }
361
+
362
+ printSection(`REPOSITORIES (${res.repos.length} RECENT)`);
363
+ res.repos.forEach((r, idx) => {
364
+ const lock = r.private ? chalk.hex('#F59E0B')('[Private]') : chalk.hex('#10B981')('[Public ]');
365
+ const num = chalk.hex('#64748B')(String(idx + 1).padStart(2, ' '));
366
+ const name = chalk.hex('#F8FAFC').bold(r.name);
367
+ const url = chalk.hex('#38BDF8')(r.clone_url);
368
+ console.log(` ${num}. ${lock} ${name}`);
369
+ console.log(` ${chalk.hex('#64748B')('URL:')} ${url}`);
370
+ });
371
+ console.log('');
372
+ return res.repos;
373
+ }
374
+
375
+ /**
376
+ * Sign out of a GitHub account (defaults to the active account).
377
+ */
378
+ export function logoutGitHub(login = null) {
379
+ const accounts = getAllGitHubAccounts();
380
+ const target = login || accounts.find((a) => a.active)?.login || accounts[0]?.login || 'GitHub';
381
+ removeGitHubToken(login);
382
+ console.log(chalk.hex('#34D399')(`\n [OK] Signed out @${target} and removed the token from this machine.\n`));
383
+ }
384
+
385
+ /**
386
+ * Interactive GitHub management menu.
387
+ */
388
+ export async function runGitHubMenu() {
389
+ while (true) {
390
+ const userStatus = await getCurrentGitHubUser();
391
+ const isLogged = userStatus.authenticated;
392
+
393
+ printSection('GITHUB & REPOSITORIES');
394
+ if (isLogged) {
395
+ console.log(chalk.hex('#34D399')(` [✓] Signed in as @${userStatus.user.login} (${userStatus.user.name || 'Developer'})`));
396
+ } else {
397
+ console.log(chalk.hex('#F59E0B')(' [!] GitHub is not connected.'));
398
+ }
399
+
400
+ const choices = [
401
+ { name: 'Account', value: 'status', description: 'View profile and connection status' },
402
+ { name: 'Repositories', value: 'list', description: 'Browse your repositories and clone URLs' },
403
+ isLogged
404
+ ? { name: 'Sign in to another account', value: 'login', description: 'Add or switch GitHub accounts' }
405
+ : { name: 'Sign in', value: 'login', description: 'Connect using a browser or token' },
406
+ ...(isLogged ? [{ name: 'Sign out', value: 'logout', description: 'Disconnect the current account' }] : []),
407
+ { name: 'Go back', value: BACK, description: 'Return to the dashboard' }
408
+ ];
409
+
410
+ const action = await selectBox({
411
+ message: 'Choose a GitHub action:',
412
+ hint: 'View accounts · browse repositories · switch accounts',
413
+ choices
414
+ });
415
+
416
+ if (action === BACK) return;
417
+
418
+ if (action === 'status') {
419
+ await showGitHubStatus();
420
+ } else if (action === 'login') {
421
+ await promptGitHubLogin();
422
+ } else if (action === 'list') {
423
+ await showUserRepos();
424
+ } else if (action === 'logout') {
425
+ logoutGitHub();
426
+ }
427
+ }
428
+ }
429
+