git-push-time 1.1.1 → 1.1.2

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,7 +1,6 @@
1
1
  import chalk from 'chalk';
2
2
  import ora from 'ora';
3
3
  import path from 'path';
4
- import { select } from '@inquirer/prompts';
5
4
  import {
6
5
  getCurrentGitHubUser,
7
6
  verifyAndFetchUser,
@@ -17,7 +16,7 @@ import {
17
16
  getAllGitHubAccounts,
18
17
  setActiveGitHubAccount
19
18
  } from '../config.js';
20
- import { selectWithBack, inputWithBack, BACK } from '../prompt-helpers.js';
19
+ import { selectWithBack, inputWithBack, selectBox, BACK } from '../prompt-helpers.js';
21
20
  import {
22
21
  printSection,
23
22
  printGitHubProfile,
@@ -110,7 +109,8 @@ async function loginViaToken() {
110
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'));
111
110
 
112
111
  const tokenInput = await inputWithBack({
113
- message: 'Dán GitHub Token của bạn:',
112
+ message: 'GitHub Token (PAT)',
113
+ hint: ['Bắt đầu bằng ghp_ hoặc github_pat_ · cần quyền repo'],
114
114
  validate: (val) => {
115
115
  if (!val || !val.trim()) return 'Vui lòng không để trống token!';
116
116
  if (val.trim().length < 15) return 'Mã token dường như quá ngắn. Vui lòng kiểm tra lại.';
@@ -166,8 +166,8 @@ export async function promptGitHubLogin() {
166
166
  * Bước chọn GitHub Account trong luồng Wizard Push
167
167
  * @returns {Promise<{ useGitHub: boolean, user?: any, back?: boolean }>}
168
168
  */
169
- export async function promptSelectGitHubAccount() {
170
- printSection('CHỌN TÀI KHOẢN GITHUB');
169
+ export async function promptSelectGitHubAccount({ clearPromptOnDone = false } = {}) {
170
+ if (!clearPromptOnDone) printSection('CHỌN TÀI KHOẢN GITHUB');
171
171
 
172
172
  const accounts = getAllGitHubAccounts();
173
173
  const currentStatus = await getCurrentGitHubUser();
@@ -176,6 +176,7 @@ export async function promptSelectGitHubAccount() {
176
176
  // Chưa có tài khoản nào
177
177
  const loginChoice = await selectWithBack({
178
178
  message: 'Bạn chưa kết nối tài khoản GitHub. Bạn muốn làm gì?',
179
+ clearPromptOnDone,
179
180
  choices: [
180
181
  {
181
182
  name: 'Đăng nhập GitHub ngay (Mở trình duyệt xác thực OAuth)',
@@ -238,6 +239,7 @@ export async function promptSelectGitHubAccount() {
238
239
 
239
240
  const picked = await selectWithBack({
240
241
  message: 'Chọn tài khoản GitHub sẽ sử dụng:',
242
+ clearPromptOnDone,
241
243
  choices
242
244
  });
243
245
 
@@ -286,7 +288,8 @@ export async function promptCreateRepo({ targetDir = process.cwd() } = {}) {
286
288
  .replace(/[^a-z0-9_.-]/g, '-');
287
289
 
288
290
  const repoName = await inputWithBack({
289
- message: 'Tên repository mới:',
291
+ message: 'Tên repository mới',
292
+ hint: ['Chỉ chữ, số, -, _ , . (VD: my-awesome-repo)'],
290
293
  default: folderName || 'my-awesome-repo',
291
294
  validate: (val) => {
292
295
  const trimmed = (val || '').trim();
@@ -301,7 +304,8 @@ export async function promptCreateRepo({ targetDir = process.cwd() } = {}) {
301
304
  if (repoName === BACK) return { ok: false, back: true };
302
305
 
303
306
  const description = await inputWithBack({
304
- message: 'Mô tả ngắn cho dự án (tùy chọn):',
307
+ message: 'Mô tả ngắn cho dự án',
308
+ hint: ['Tùy chọn, Enter để bỏ qua'],
305
309
  default: ''
306
310
  });
307
311
  if (description === BACK) return { ok: false, back: true };
@@ -404,8 +408,9 @@ export async function runGitHubMenu({ defaultDir = process.cwd() } = {}) {
404
408
  { name: 'Quay lại', value: BACK, description: 'Về bảng điều khiển' }
405
409
  ];
406
410
 
407
- const action = await select({
411
+ const action = await selectBox({
408
412
  message: 'Chọn tác vụ GitHub:',
413
+ hint: 'Tạo repo · xem profile · đổi tài khoản',
409
414
  choices
410
415
  });
411
416
 
@@ -418,8 +423,9 @@ export async function runGitHubMenu({ defaultDir = process.cwd() } = {}) {
418
423
  } else if (action === 'create') {
419
424
  const res = await promptCreateRepo({ targetDir: defaultDir });
420
425
  if (res.ok && res.repo) {
421
- const askPush = await select({
422
- message: 'Bạn có muốn thiết lập remote và đẩy dự án hiện tại lên repo mới này ngay không?',
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',
423
429
  choices: [
424
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' },
425
431
  { name: 'Không, chỉ tạo repo thôi', value: 'skip', description: 'Hoàn tất' }
@@ -1,5 +1,5 @@
1
1
  import chalk from 'chalk';
2
- import { select } from '@inquirer/prompts';
2
+ import { selectBox } from '../prompt-helpers.js';
3
3
  import {
4
4
  checkGitInstalled,
5
5
  isGitRepo,
@@ -65,8 +65,9 @@ export async function runPullCommand(remote = 'origin', branch, options = {}) {
65
65
  process.exit(1);
66
66
  }
67
67
 
68
- const choice = await select({
68
+ const choice = await selectBox({
69
69
  message: 'Pull fast-forward thất bại. Bạn muốn làm gì tiếp?',
70
+ hint: `git pull --rebase ${remote} ${branch}`,
70
71
  choices: [
71
72
  { name: 'Thử kéo về bằng pull --rebase', value: 'rebase', description: `git pull --rebase ${remote} ${branch}` },
72
73
  { name: 'Quay lại', value: 'back', description: 'Về màn hình trước' },
@@ -3,7 +3,6 @@ import ora from 'ora';
3
3
  import fs from 'fs';
4
4
  import path from 'path';
5
5
  import { spawnSync } from 'child_process';
6
- import { select } from '@inquirer/prompts';
7
6
  import { isValidGitUrl, parseCustomDate } from '../utils.js';
8
7
  import {
9
8
  isGitRepo,
@@ -34,12 +33,12 @@ import {
34
33
  listUserRepos
35
34
  } from '../github.js';
36
35
  import { promptCreateRepo, promptSelectGitHubAccount } from './github.js';
37
- import { selectWithBack, inputWithBack, promptContinueOrBack, BACK } from '../prompt-helpers.js';
36
+ import { selectWithBack, inputWithBack, promptContinueOrBack, selectBox, BACK } from '../prompt-helpers.js';
38
37
  import {
39
38
  printSummary,
40
39
  printSection,
40
+ printPushProgress,
41
41
  stepLabel,
42
- printTargetDir,
43
42
  printNestedRepoWarning,
44
43
  printPrePushSecretWarning,
45
44
  printSecretBlockedBox,
@@ -53,19 +52,18 @@ import {
53
52
  } from '../ui.js';
54
53
 
55
54
  const TOTAL_STEPS = 6;
55
+ const WIZARD_STEPS = 7;
56
56
 
57
57
  /**
58
58
  * Cho user chọn folder nguồn cần push
59
59
  */
60
- export async function promptForTargetDir(startDir) {
60
+ export async function promptForTargetDir(startDir, clearPromptOnDone = false) {
61
61
  const subs = listSubdirectories(startDir);
62
62
  if (subs.length === 0) return startDir;
63
63
 
64
- printSection('📁 1. CHỌN FOLDER DỰ ÁN (PROJECT)');
65
- printTargetDir(startDir);
66
-
67
64
  const picked = await selectWithBack({
68
65
  message: 'Thư mục nào sẽ được đẩy lên Git?',
66
+ clearPromptOnDone,
69
67
  pageSize: Math.min(12, subs.length + 3),
70
68
  choices: [
71
69
  { name: 'Thư mục hiện tại (.)', value: startDir, description: 'Push toàn bộ thư mục hiện tại' },
@@ -82,7 +80,9 @@ export async function promptForTargetDir(startDir) {
82
80
  if (picked !== '__custom__') return picked;
83
81
 
84
82
  const customPath = await inputWithBack({
85
- message: 'Nhập đường dẫn folder nguồn:',
83
+ message: 'Đường dẫn folder nguồn',
84
+ hint: ['Nhập tuyệt đối hoặc tương đối (VD: ./my-app, E:/projects/demo)'],
85
+ clearPromptOnDone,
86
86
  validate: (value) => {
87
87
  const r = resolveTargetDir(value, startDir);
88
88
  return r.ok || r.error;
@@ -135,9 +135,7 @@ export async function handleNestedRepo(targetDir, isInteractive, options) {
135
135
  /**
136
136
  * Prompt chọn hoặc tạo repository URL
137
137
  */
138
- async function promptForRepoSelection(targetDir, gitHubUser = null) {
139
- printSection('📦 3. CHỌN REPOSITORY');
140
-
138
+ async function promptForRepoSelection(targetDir, gitHubUser = null, clearPromptOnDone = false) {
141
139
  const choices = [];
142
140
 
143
141
  if (gitHubUser) {
@@ -171,6 +169,7 @@ async function promptForRepoSelection(targetDir, gitHubUser = null) {
171
169
 
172
170
  const method = await selectWithBack({
173
171
  message: 'Bạn muốn chọn repository bằng cách nào?',
172
+ clearPromptOnDone,
174
173
  choices
175
174
  });
176
175
 
@@ -198,6 +197,7 @@ async function promptForRepoSelection(targetDir, gitHubUser = null) {
198
197
 
199
198
  const picked = await selectWithBack({
200
199
  message: 'Chọn repository bạn muốn push lên:',
200
+ clearPromptOnDone,
201
201
  pageSize: Math.min(12, repoChoices.length + 2),
202
202
  choices: repoChoices
203
203
  });
@@ -208,7 +208,9 @@ async function promptForRepoSelection(targetDir, gitHubUser = null) {
208
208
 
209
209
  // Dán URL thủ công
210
210
  const val = await inputWithBack({
211
- message: 'Dán link Git Repository:',
211
+ message: 'Link Git Repository',
212
+ hint: ['HTTPS hoặc SSH (VD: https://github.com/user/repo.git)'],
213
+ clearPromptOnDone,
212
214
  validate: (value) => {
213
215
  if (!value || !value.trim()) return 'Vui lòng nhập đường dẫn repository!';
214
216
  if (!isValidGitUrl(value)) return 'URL chưa đúng (vd: https://github.com/user/repo.git)';
@@ -233,8 +235,8 @@ export async function runPushWizard(options = {}, onBackToMenu = null) {
233
235
  let targetDir = startDir;
234
236
  let selectedGitHubUser = null;
235
237
  let repoUrl = eff.repo;
236
- let branchName = eff.branch;
237
- let commitMsg = eff.message;
238
+ let branchName = eff.branch ? String(eff.branch).trim() : undefined;
239
+ let commitMsg = eff.message ? String(eff.message).trim() : undefined;
238
240
  let dateInput = eff.date;
239
241
  let parsedDate = null;
240
242
 
@@ -280,9 +282,22 @@ export async function runPushWizard(options = {}, onBackToMenu = null) {
280
282
  }
281
283
 
282
284
  while (true) {
285
+ const progressTitles = ['Folder', 'Account', 'Repository', 'Branch', 'Message', 'Time', 'Xác nhận'];
286
+ const completed = [];
287
+ if (step > 0) completed.push({ label: 'Folder', value: targetDir });
288
+ if (step > 1) completed.push({ label: 'Account', value: selectedGitHubUser?.login || 'URL thủ công' });
289
+ if (step > 2) {
290
+ const shortRepo = String(repoUrl || '').replace(/\.git$/i, '').match(/(?:github\.com[:/])([^/]+\/[^/]+)$/i)?.[1] || repoUrl;
291
+ completed.push({ label: 'Repository', value: shortRepo || 'Đã chọn' });
292
+ }
293
+ if (step > 3) completed.push({ label: 'Branch', value: branchName || 'main' });
294
+ if (step > 4) completed.push({ label: 'Message', value: commitMsg || 'Initial commit' });
295
+ if (step > 5) completed.push({ label: 'Time', value: parsedDate?.display || dateInput || 'now' });
296
+ printPushProgress({ current: step + 1, total: WIZARD_STEPS, title: progressTitles[step], completed });
297
+
283
298
  // BƯỚC 0: 📁 Chọn folder project
284
299
  if (step === 0) {
285
- const picked = await promptForTargetDir(startDir);
300
+ const picked = await promptForTargetDir(startDir, true);
286
301
  if (picked === BACK) {
287
302
  if (onBackToMenu) return await onBackToMenu();
288
303
  printCancelled();
@@ -298,7 +313,7 @@ export async function runPushWizard(options = {}, onBackToMenu = null) {
298
313
 
299
314
  // BƯỚC 1: 👤 Chọn GitHub account
300
315
  if (step === 1) {
301
- const accRes = await promptSelectGitHubAccount();
316
+ const accRes = await promptSelectGitHubAccount({ clearPromptOnDone: true });
302
317
  if (accRes.back) {
303
318
  // Cho phép quay về chọn folder dù ban đầu có --dir
304
319
  delete eff.dir;
@@ -318,7 +333,7 @@ export async function runPushWizard(options = {}, onBackToMenu = null) {
318
333
  step = 3;
319
334
  continue;
320
335
  }
321
- const pickedUrl = await promptForRepoSelection(targetDir, selectedGitHubUser);
336
+ const pickedUrl = await promptForRepoSelection(targetDir, selectedGitHubUser, true);
322
337
  if (pickedUrl === BACK) {
323
338
  step = 1;
324
339
  continue;
@@ -338,28 +353,33 @@ export async function runPushWizard(options = {}, onBackToMenu = null) {
338
353
  // BƯỚC 3: 🌿 Chọn branch
339
354
  if (step === 3) {
340
355
  if (eff.branch) {
341
- branchName = eff.branch;
356
+ branchName = String(eff.branch).trim();
342
357
  delete eff.branch;
343
358
  step = 4;
344
359
  continue;
345
360
  }
346
- printSection('🌿 4. CHỌN BRANCH REMOTE');
347
361
  const currentB = getCurrentBranch(targetDir);
348
362
  const val = await inputWithBack({
349
- message: 'Tên branch trên remote:',
363
+ message: 'Đặt tên nhánh Git',
364
+ promptLabel: 'Tên nhánh',
365
+ hint: [
366
+ 'Nhánh local sẽ được đổi tên này trước khi push lên origin.',
367
+ 'Ví dụ: main, dev, feature/login.',
368
+ ],
350
369
  default: currentB || 'main',
370
+ clearPromptOnDone: true,
351
371
  validate: (v) => {
352
372
  if (!v || !v.trim()) return true;
353
- return isValidBranchName(v.trim()) || 'Tên branch chứa ký tự không hợp lệ (cấm space, ~ ^ : ? * [ \\ ..)';
373
+ return isValidBranchName(v.trim()) || 'Tên nhánh không hợp lệ: không dùng dấu cách, .., ~ ^ : ? * [ \\ hoặc @{.';
354
374
  }
355
375
  });
356
376
  if (val === BACK) {
357
377
  step = 2;
358
378
  continue;
359
379
  }
360
- branchName = val || currentB || 'main';
380
+ branchName = String(val || currentB || 'main').trim();
361
381
  if (!isValidBranchName(branchName)) {
362
- printErrorBox('BRANCH KHÔNG HỢP LỆ', [`Tên branch: '${branchName}'`]);
382
+ printErrorBox('TÊN NHÁNH KHÔNG HỢP LỆ', [`Tên nhánh: '${branchName}'`]);
363
383
  continue;
364
384
  }
365
385
  step = 4;
@@ -369,21 +389,26 @@ export async function runPushWizard(options = {}, onBackToMenu = null) {
369
389
  // BƯỚC 4: 💬 Nhập commit message
370
390
  if (step === 4) {
371
391
  if (eff.message) {
372
- commitMsg = eff.message;
392
+ commitMsg = String(eff.message).trim();
373
393
  delete eff.message;
374
394
  step = 5;
375
395
  continue;
376
396
  }
377
- printSection('💬 5. NHẬP COMMIT MESSAGE');
378
397
  const val = await inputWithBack({
379
- message: 'Thông điệp commit (Message):',
380
- default: 'Initial commit'
398
+ message: 'Viết commit message',
399
+ promptLabel: 'Commit',
400
+ hint: [
401
+ 'Mô tả ngắn thay đổi bạn sắp đẩy.',
402
+ 'Ví dụ: feat: thêm đăng nhập · fix: sửa lỗi push.',
403
+ ],
404
+ default: 'Initial commit',
405
+ clearPromptOnDone: true
381
406
  });
382
407
  if (val === BACK) {
383
408
  step = 3;
384
409
  continue;
385
410
  }
386
- commitMsg = val || 'Initial commit';
411
+ commitMsg = String(val || '').trim() || 'Initial commit';
387
412
  step = 5;
388
413
  continue;
389
414
  }
@@ -396,9 +421,10 @@ export async function runPushWizard(options = {}, onBackToMenu = null) {
396
421
  step = 6;
397
422
  continue;
398
423
  }
399
- printSection('⏰ 6. CHỌN THỜI GIAN COMMIT');
400
424
  const dateOption = await selectWithBack({
401
- message: 'Thời gian cho commit này:',
425
+ message: 'Bước 6/7 · Thời gian commit',
426
+ hint: 'Chọn mốc thời gian ghi vào commit',
427
+ clearPromptOnDone: true,
402
428
  choices: [
403
429
  { name: 'Ngay bây giờ', value: 'now', description: 'Sử dụng thời gian hiện tại' },
404
430
  { name: 'Lùi thời gian (Backdate)', value: 'relative', description: 'VD: -2d (2 ngày trước), -5h, -30m, -1w' },
@@ -414,16 +440,20 @@ export async function runPushWizard(options = {}, onBackToMenu = null) {
414
440
  dateInput = 'now';
415
441
  } else if (dateOption === 'relative') {
416
442
  const val = await inputWithBack({
417
- message: 'Nhập thời gian lùi (VD: -2d, -5h, -30m, -1w):',
443
+ message: 'Lùi thời gian commit (Backdate)',
444
+ hint: ['-2d = 2 ngày trước · -5h = 5 giờ trước · -30m = 30 phút trước · -1w = 1 tuần trước'],
418
445
  default: '-1d',
446
+ clearPromptOnDone: true,
419
447
  validate: (v) => { const p = parseCustomDate(v); return p.valid || p.error || 'Thời gian không hợp lệ'; }
420
448
  });
421
449
  if (val === BACK) continue;
422
450
  dateInput = val;
423
451
  } else if (dateOption === 'custom') {
424
452
  const val = await inputWithBack({
425
- message: 'Nhập ngày giờ (YYYY-MM-DD HH:mm:ss):',
453
+ message: 'Ngày giờ cụ thể cho commit',
454
+ hint: ['Định dạng: YYYY-MM-DD HH:mm:ss (VD: 2024-01-15 10:00:00)'],
426
455
  default: '2024-01-15 10:00:00',
456
+ clearPromptOnDone: true,
427
457
  validate: (v) => { const p = parseCustomDate(v); return p.valid || p.error || 'Ngày giờ không hợp lệ'; }
428
458
  });
429
459
  if (val === BACK) continue;
@@ -460,8 +490,9 @@ export async function runPushWizard(options = {}, onBackToMenu = null) {
460
490
  sourceDir: targetDir
461
491
  });
462
492
 
463
- const decision = await select({
493
+ const decision = await selectBox({
464
494
  message: 'Xác nhận đẩy dự án lên Git ngay?',
495
+ hint: 'Kiểm tra tóm tắt bên trên trước khi push',
465
496
  choices: [
466
497
  { name: 'Bắt đầu đẩy code (Push)', value: 'continue', description: 'Thực thi git init/add/commit/push' },
467
498
  { name: 'Sửa thời gian commit', value: 'back-date', description: 'Về bước chọn thời gian' },
@@ -494,6 +525,8 @@ export async function runPushWizard(options = {}, onBackToMenu = null) {
494
525
 
495
526
  // 🚀 BƯỚC 7: THỰC THI PUSH
496
527
  const spinner = ora({ color: 'cyan' });
528
+ let secretStatus = '✓ Không phát hiện';
529
+ let pushDurationSeconds = null;
497
530
  printSection(`🚀 ĐANG ĐẨY CODE LÊN GIT (${TOTAL_STEPS} BƯỚC)`);
498
531
 
499
532
  try {
@@ -519,6 +552,7 @@ export async function runPushWizard(options = {}, onBackToMenu = null) {
519
552
  // 3.5. Quét secret nhạy cảm
520
553
  const risky = findRiskyStagedFiles(targetDir);
521
554
  const findings = scanStagedContentForSecrets(targetDir);
555
+ if (risky.length > 0 || findings.length > 0) secretStatus = `! Có ${risky.length + findings.length} cảnh báo`;
522
556
  if (risky.length > 0 || findings.length > 0) {
523
557
  spinner.stop();
524
558
  printPrePushSecretWarning({ riskyFiles: risky, findings });
@@ -546,6 +580,12 @@ export async function runPushWizard(options = {}, onBackToMenu = null) {
546
580
  spinner.succeed(chalk.hex('#34D399')(`[OK] Đã gỡ ${res.fixed.length} file khỏi commit`));
547
581
  printFixResult(res);
548
582
 
583
+ const remainingRisky = findRiskyStagedFiles(targetDir);
584
+ const remainingFindings = scanStagedContentForSecrets(targetDir);
585
+ secretStatus = remainingRisky.length || remainingFindings.length
586
+ ? `! Còn ${remainingRisky.length + remainingFindings.length} cảnh báo`
587
+ : '✓ Đã gỡ cảnh báo';
588
+
549
589
  spawnSync('git', ['add', '.gitignore'], { cwd: targetDir, stdio: 'ignore' });
550
590
 
551
591
  if (!hasStagedChanges(targetDir)) {
@@ -576,10 +616,12 @@ export async function runPushWizard(options = {}, onBackToMenu = null) {
576
616
 
577
617
  // 6. Push
578
618
  spinner.start(stepLabel(6, TOTAL_STEPS, `Push lên origin/${branchName}...`));
619
+ const pushStartedAt = Date.now();
579
620
  pushToRemote(branchName, options.force, targetDir);
621
+ pushDurationSeconds = (Date.now() - pushStartedAt) / 1000;
580
622
  spinner.succeed(stepLabel(6, TOTAL_STEPS, chalk.bold.hex('#34D399')('Push lên remote thành công!')));
581
623
 
582
- printSuccessBox({ branch: branchName, repoUrl });
624
+ printSuccessBox({ branch: branchName, repoUrl, durationSeconds: pushDurationSeconds, secretStatus });
583
625
  printFooter();
584
626
  } catch (error) {
585
627
  spinner.stop();
@@ -607,9 +649,11 @@ export async function runPushWizard(options = {}, onBackToMenu = null) {
607
649
  spinner.succeed(chalk.green('[OK] Đã làm sạch commit! Đang thử push lại...'));
608
650
  try {
609
651
  spinner.start(stepLabel(6, TOTAL_STEPS, `Push lại lên origin/${branchName}...`));
652
+ const retryStartedAt = Date.now();
610
653
  pushToRemote(branchName, options.force, targetDir);
654
+ pushDurationSeconds = (Date.now() - retryStartedAt) / 1000;
611
655
  spinner.succeed(chalk.bold.green('[OK] Push thành công'));
612
- printSuccessBox({ branch: branchName, repoUrl });
656
+ printSuccessBox({ branch: branchName, repoUrl, durationSeconds: pushDurationSeconds, secretStatus });
613
657
  printFooter();
614
658
  return;
615
659
  } catch (retryErr) {
@@ -625,8 +669,9 @@ export async function runPushWizard(options = {}, onBackToMenu = null) {
625
669
  } else if (error.code === 'NON_FAST_FORWARD') {
626
670
  printNonFastForwardBox(error.message, branchName);
627
671
  if (isInteractive) {
628
- const ffChoice = await select({
672
+ const ffChoice = await selectBox({
629
673
  message: 'Remote đã có commit mới hơn. Bạn muốn xử lý thế nào?',
674
+ hint: `git pull --rebase origin ${branchName} rồi push lại`,
630
675
  choices: [
631
676
  { name: 'Pull (rebase) rồi push lại — Khuyên dùng', value: 'pull-push', description: `git pull --rebase origin ${branchName} rồi push lại` },
632
677
  { name: 'Chỉ Pull về (chưa push vội)', value: 'pull-only', description: 'Chỉ đồng bộ code về máy' },
@@ -642,9 +687,11 @@ export async function runPushWizard(options = {}, onBackToMenu = null) {
642
687
  if (pullOut) console.log(chalk.hex('#64748B')(` ${String(pullOut).split('\n')[0].slice(0, 120)}`));
643
688
  if (ffChoice === 'pull-push') {
644
689
  spinner.start(stepLabel(6, TOTAL_STEPS, `Push lại lên origin/${branchName}...`));
690
+ const retryStartedAt = Date.now();
645
691
  pushToRemote(branchName, options.force, targetDir);
692
+ pushDurationSeconds = (Date.now() - retryStartedAt) / 1000;
646
693
  spinner.succeed(chalk.bold.hex('#34D399')('Push lại thành công sau khi pull!'));
647
- printSuccessBox({ branch: branchName, repoUrl });
694
+ printSuccessBox({ branch: branchName, repoUrl, durationSeconds: pushDurationSeconds, secretStatus });
648
695
  printFooter();
649
696
  return;
650
697
  }
package/lib/git.js CHANGED
@@ -116,6 +116,30 @@ export function getCurrentBranch(cwd = process.cwd()) {
116
116
  return getCurrentBranchStrict(cwd) || 'main';
117
117
  }
118
118
 
119
+ /** Return the current local change count, origin URL, and last commit for the dashboard. */
120
+ export function getDashboardGitStatus(cwd = process.cwd()) {
121
+ const status = spawnSync('git', ['status', '--short'], { cwd, encoding: 'utf-8' });
122
+ const remote = spawnSync('git', ['remote', 'get-url', 'origin'], { cwd, encoding: 'utf-8' });
123
+ const log = spawnSync('git', ['log', '-1', '--format=%h|%s|%cr'], { cwd, encoding: 'utf-8' });
124
+ const changes = status.status === 0
125
+ ? (status.stdout || '').split(/\r?\n/).filter(Boolean).length
126
+ : 0;
127
+
128
+ let lastCommit = null;
129
+ if (log.status === 0 && (log.stdout || '').trim()) {
130
+ const [hash, msg, rel] = (log.stdout || '').trim().split('|');
131
+ if (hash) {
132
+ lastCommit = { hash, message: msg || '', relativeTime: rel || '' };
133
+ }
134
+ }
135
+
136
+ return {
137
+ changes,
138
+ remoteUrl: remote.status === 0 ? (remote.stdout || '').trim() : null,
139
+ lastCommit,
140
+ };
141
+ }
142
+
119
143
  /**
120
144
  * Lấy tên branch hiện tại, trả về null khi detached HEAD hoặc lỗi.
121
145
  * @param {string} cwd