git-push-time 1.1.3 → 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.
@@ -55,24 +55,24 @@ const TOTAL_STEPS = 6;
55
55
  const WIZARD_STEPS = 7;
56
56
 
57
57
  /**
58
- * Cho user chọn folder nguồn cần push
58
+ * Let the user choose the source folder to push.
59
59
  */
60
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
64
  const picked = await selectWithBack({
65
- message: 'Thư mục nào sẽ được đẩy lên Git?',
65
+ message: 'Which project folder should be pushed to Git?',
66
66
  clearPromptOnDone,
67
67
  pageSize: Math.min(12, subs.length + 3),
68
68
  choices: [
69
- { name: 'Thư mục hiện tại (.)', value: startDir, description: 'Push toàn bộ thư mục hiện tại' },
69
+ { name: 'Current folder (.)', value: startDir, description: 'Push the entire current folder' },
70
70
  ...subs.map((s) => ({
71
71
  name: `${s.name}`,
72
72
  value: s.fullPath,
73
73
  description: s.fullPath
74
74
  })),
75
- { name: 'Nhập đường dẫn khác...', value: '__custom__', description: 'Gõ tay đường dẫn tuyệt đối hoặc tương đối' }
75
+ { name: 'Enter another path...', value: '__custom__', description: 'Type an absolute or relative path' }
76
76
  ]
77
77
  });
78
78
 
@@ -80,8 +80,8 @@ export async function promptForTargetDir(startDir, clearPromptOnDone = false) {
80
80
  if (picked !== '__custom__') return picked;
81
81
 
82
82
  const customPath = await inputWithBack({
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)'],
83
+ message: 'Project folder path',
84
+ hint: ['Enter an absolute or relative path (e.g. ./my-app, E:/projects/demo)'],
85
85
  clearPromptOnDone,
86
86
  validate: (value) => {
87
87
  const r = resolveTargetDir(value, startDir);
@@ -93,7 +93,7 @@ export async function promptForTargetDir(startDir, clearPromptOnDone = false) {
93
93
  }
94
94
 
95
95
  /**
96
- * Xử lý khi thư mục nằm lồng trong repo cha
96
+ * Handle a folder nested inside a parent repository.
97
97
  */
98
98
  export async function handleNestedRepo(targetDir, isInteractive, options) {
99
99
  const topLevel = getGitTopLevel(targetDir);
@@ -106,11 +106,11 @@ export async function handleNestedRepo(targetDir, isInteractive, options) {
106
106
  let choice = 'separate';
107
107
  if (isInteractive && !options.yes) {
108
108
  choice = await selectWithBack({
109
- message: 'Bạn muốn push thế nào?',
109
+ message: 'How would you like to push this folder?',
110
110
  choices: [
111
- { name: 'Tạo repo RIÊNG chỉ cho folder này (khuyên dùng)', value: 'separate', description: 'git init trong folder này, chỉ up nội dung của nó' },
112
- { name: 'Dùng chung repo của folder cha', value: 'shared', description: 'Giữ nguyên, push theo repo cha' },
113
- { name: 'x Hủy để kiểm tra lại', value: 'abort', description: 'Thoát, không làm gì' }
111
+ { name: 'Create a SEPARATE repository for this folder (recommended)', value: 'separate', description: 'Run git init here and push only this folder' },
112
+ { name: 'Use the parent repository', value: 'shared', description: 'Keep the parent repository and push from it' },
113
+ { name: 'Cancel and review', value: 'abort', description: 'Exit without making changes' }
114
114
  ]
115
115
  });
116
116
  if (choice === BACK) return BACK;
@@ -126,14 +126,14 @@ export async function handleNestedRepo(targetDir, isInteractive, options) {
126
126
  }
127
127
  if (choice === 'separate') {
128
128
  initGitRepo(targetDir);
129
- console.log(chalk.hex('#34D399')(' [OK] Đã tạo Git repo riêng cho folder này.\n'));
129
+ console.log(chalk.hex('#34D399')(' [OK] Created a separate Git repository for this folder.\n'));
130
130
  } else {
131
- console.log(chalk.hex('#64748B')(' » Dùng chung repo cha (chỉ stage file trong folder này).\n'));
131
+ console.log(chalk.hex('#64748B')(' » Using the parent repository (only files in this folder will be staged).\n'));
132
132
  }
133
133
  }
134
134
 
135
135
  /**
136
- * Prompt chọn hoặc tạo repository URL
136
+ * Prompt the user to select or create a repository URL.
137
137
  */
138
138
  async function promptForRepoSelection(targetDir, gitHubUser = null, clearPromptOnDone = false) {
139
139
  const choices = [];
@@ -141,34 +141,34 @@ async function promptForRepoSelection(targetDir, gitHubUser = null, clearPromptO
141
141
  if (gitHubUser) {
142
142
  choices.push(
143
143
  {
144
- name: `Chọn từ danh sách Repo của @${gitHubUser.login}`,
144
+ name: `Choose from @${gitHubUser.login}'s repositories`,
145
145
  value: 'pick_existing',
146
- description: 'Chọn nhanh từ các repo gần nhất của bạn trên GitHub'
146
+ description: 'Quickly choose one of your recently updated GitHub repositories'
147
147
  },
148
148
  {
149
- name: 'Tạo repository MỚI trên GitHub ngay lập tức',
149
+ name: 'Create a NEW GitHub repository',
150
150
  value: 'create_repo',
151
- description: `Tự động tạo repo mới trên tài khoản @${gitHubUser.login}`
151
+ description: `Create a repository under @${gitHubUser.login}`
152
152
  }
153
153
  );
154
154
  } else {
155
155
  choices.push(
156
156
  {
157
- name: 'Tạo repository MỚI trên GitHub',
157
+ name: 'Create a NEW GitHub repository',
158
158
  value: 'create_repo',
159
- description: 'Đăng nhập và tạo repository mới'
159
+ description: 'Sign in and create a repository'
160
160
  }
161
161
  );
162
162
  }
163
163
 
164
164
  choices.push({
165
- name: 'Dán URL Git Repository thủ công',
165
+ name: 'Enter a Git repository URL manually',
166
166
  value: 'manual_url',
167
- description: 'Dán đường dẫn HTTPS hoặc SSH (GitHub, GitLab, Bitbucket...)'
167
+ description: 'Enter an HTTPS or SSH URL (GitHub, GitLab, Bitbucket...)'
168
168
  });
169
169
 
170
170
  const method = await selectWithBack({
171
- message: 'Bạn muốn chọn repository bằng cách nào?',
171
+ message: 'How would you like to choose a repository?',
172
172
  clearPromptOnDone,
173
173
  choices
174
174
  });
@@ -182,12 +182,12 @@ async function promptForRepoSelection(targetDir, gitHubUser = null, clearPromptO
182
182
  }
183
183
 
184
184
  if (method === 'pick_existing') {
185
- const spinner = ora({ text: 'Đang tải danh sách repo từ GitHub…', color: 'cyan' }).start();
185
+ const spinner = ora({ text: 'Loading repositories from GitHub…', color: 'cyan' }).start();
186
186
  const res = await listUserRepos({ limit: 25 });
187
187
  spinner.stop();
188
188
 
189
189
  if (!res.ok || !res.repos || res.repos.length === 0) {
190
- console.log(chalk.hex('#F59E0B')(' [!] Không tìm thấy repo nào trên tài khoản này.'));
190
+ console.log(chalk.hex('#F59E0B')(' [!] No repositories found for this account.'));
191
191
  } else {
192
192
  const repoChoices = res.repos.map((r) => ({
193
193
  name: `${r.private ? chalk.hex('#F59E0B')('[Private]') : chalk.hex('#10B981')('[Public ]')} ${chalk.hex('#F8FAFC').bold(r.name)}`,
@@ -196,7 +196,7 @@ async function promptForRepoSelection(targetDir, gitHubUser = null, clearPromptO
196
196
  }));
197
197
 
198
198
  const picked = await selectWithBack({
199
- message: 'Chọn repository bạn muốn push lên:',
199
+ message: 'Choose the repository to push to:',
200
200
  clearPromptOnDone,
201
201
  pageSize: Math.min(12, repoChoices.length + 2),
202
202
  choices: repoChoices
@@ -206,14 +206,14 @@ async function promptForRepoSelection(targetDir, gitHubUser = null, clearPromptO
206
206
  }
207
207
  }
208
208
 
209
- // Dán URL thủ công
209
+ // Enter a URL manually.
210
210
  const val = await inputWithBack({
211
- message: 'Link Git Repository',
212
- hint: ['HTTPS hoặc SSH (VD: https://github.com/user/repo.git)'],
211
+ message: 'Git repository URL',
212
+ hint: ['HTTPS or SSH (e.g. https://github.com/user/repo.git)'],
213
213
  clearPromptOnDone,
214
214
  validate: (value) => {
215
- if (!value || !value.trim()) return 'Vui lòng nhập đường dẫn repository!';
216
- if (!isValidGitUrl(value)) return 'URL chưa đúng (vd: https://github.com/user/repo.git)';
215
+ if (!value || !value.trim()) return 'Enter a repository URL.';
216
+ if (!isValidGitUrl(value)) return 'Invalid URL (e.g. https://github.com/user/repo.git).';
217
217
  return true;
218
218
  }
219
219
  });
@@ -222,14 +222,14 @@ async function promptForRepoSelection(targetDir, gitHubUser = null, clearPromptO
222
222
  }
223
223
 
224
224
  /**
225
- * Thực thi luồng Push chuẩn 7 bước
225
+ * Run the standard seven-step push flow.
226
226
  */
227
227
  export async function runPushWizard(options = {}, onBackToMenu = null) {
228
228
  const isInteractive = !options.yes;
229
229
  const fromMenu = typeof onBackToMenu === 'function';
230
230
  const startDir = process.cwd();
231
- // Copy flags sang object mutable để Back-navigation có thể xóa flag đã consume
232
- // (tránh loop vô hạn khi --repo/--branch/--message/--date còn truthy).
231
+ // Copy flags into a mutable object so back navigation can clear consumed flags
232
+ // and avoid looping when --repo/--branch/--message/--date remain truthy.
233
233
  const eff = { ...options };
234
234
 
235
235
  let targetDir = startDir;
@@ -243,7 +243,7 @@ export async function runPushWizard(options = {}, onBackToMenu = null) {
243
243
  if (eff.dir) {
244
244
  const r = resolveTargetDir(eff.dir, startDir);
245
245
  if (!r.ok) {
246
- printErrorBox('THƯ MỤC NGUỒN KHÔNG HỢP LỆ', [r.error], 'VD: --dir ./my-app hoặc --dir "E:/projects/my-app"');
246
+ printErrorBox('INVALID SOURCE DIRECTORY', [r.error], 'Example: --dir ./my-app or --dir "E:/projects/my-app"');
247
247
  process.exit(1);
248
248
  }
249
249
  targetDir = r.fullPath;
@@ -254,27 +254,27 @@ export async function runPushWizard(options = {}, onBackToMenu = null) {
254
254
  if (!dateInput) dateInput = 'now';
255
255
  if (!branchName) branchName = 'main';
256
256
  if (!repoUrl || !isValidGitUrl(repoUrl)) {
257
- printErrorBox('URL REPOSITORY KHÔNG HỢP LỆ', [`URL nhận được: '${repoUrl || '(trống)'}'`], 'Ví dụ đúng: https://github.com/username/repository.git');
257
+ printErrorBox('INVALID REPOSITORY URL', [`Received URL: '${repoUrl || '(empty)'}'`], 'Example: https://github.com/username/repository.git');
258
258
  process.exit(1);
259
259
  }
260
260
  parsedDate = parseCustomDate(dateInput);
261
261
  if (!parsedDate.valid) {
262
- printErrorBox('THỜI GIAN KHÔNG HỢP LỆ', [parsedDate.error], 'VD: now, -2d, -5h, 2024-01-15 14:30:00');
262
+ printErrorBox('INVALID DATE OR TIME', [parsedDate.error], 'Examples: now, -2d, -5h, 2024-01-15 14:30:00');
263
263
  process.exit(1);
264
264
  }
265
265
  printSummary({ repoUrl, commitMsg, dateDisplay: parsedDate.display, branch: branchName, force: options.force, sourceDir: targetDir });
266
266
  } else {
267
- // Luồng tương tác 7 bước chuẩn:
268
- // 0: 📁 Chọn folder
269
- // 1: 👤 Chọn GitHub account
270
- // 2: 📦 Chọn repository
271
- // 3: 🌿 Chọn branch
272
- // 4: 💬 Nhập commit message
273
- // 5: ⏰ Chọn commit time
274
- // 6: 🚀 Xác nhận & Push
267
+ // Standard seven-step interactive flow:
268
+ // 0: 📁 Choose folder
269
+ // 1: 👤 Choose GitHub account
270
+ // 2: 📦 Choose repository
271
+ // 3: 🌿 Choose branch
272
+ // 4: 💬 Enter commit message
273
+ // 5: ⏰ Choose commit time
274
+ // 6: 🚀 Review and push
275
275
  let step = eff.dir ? 1 : 0;
276
276
 
277
- // Nếu đã truyền sẵn URL qua flag thì có thể nhảy bước chọn repo (chỉ consume 1 lần)
277
+ // Skip repository selection once when a URL was supplied as a flag.
278
278
  if (eff.repo) {
279
279
  repoUrl = eff.repo;
280
280
  delete eff.repo;
@@ -282,20 +282,20 @@ export async function runPushWizard(options = {}, onBackToMenu = null) {
282
282
  }
283
283
 
284
284
  while (true) {
285
- const progressTitles = ['Folder', 'Account', 'Repository', 'Branch', 'Message', 'Time', 'Xác nhận'];
285
+ const progressTitles = ['Folder', 'Account', 'Repository', 'Branch', 'Message', 'Time', 'Confirm'];
286
286
  const completed = [];
287
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' });
288
+ if (step > 1) completed.push({ label: 'Account', value: selectedGitHubUser?.login || 'Manual URL' });
289
289
  if (step > 2) {
290
290
  const shortRepo = String(repoUrl || '').replace(/\.git$/i, '').match(/(?:github\.com[:/])([^/]+\/[^/]+)$/i)?.[1] || repoUrl;
291
- completed.push({ label: 'Repository', value: shortRepo || 'Đã chọn' });
291
+ completed.push({ label: 'Repository', value: shortRepo || 'Selected' });
292
292
  }
293
293
  if (step > 3) completed.push({ label: 'Branch', value: branchName || 'main' });
294
294
  if (step > 4) completed.push({ label: 'Message', value: commitMsg || 'Initial commit' });
295
295
  if (step > 5) completed.push({ label: 'Time', value: parsedDate?.display || dateInput || 'now' });
296
296
  printPushProgress({ current: step + 1, total: WIZARD_STEPS, title: progressTitles[step], completed });
297
297
 
298
- // BƯỚC 0: 📁 Chọn folder project
298
+ // STEP 0: 📁 Choose project folder.
299
299
  if (step === 0) {
300
300
  const picked = await promptForTargetDir(startDir, true);
301
301
  if (picked === BACK) {
@@ -311,11 +311,11 @@ export async function runPushWizard(options = {}, onBackToMenu = null) {
311
311
  continue;
312
312
  }
313
313
 
314
- // BƯỚC 1: 👤 Chọn GitHub account
314
+ // STEP 1: 👤 Choose GitHub account.
315
315
  if (step === 1) {
316
316
  const accRes = await promptSelectGitHubAccount({ clearPromptOnDone: true });
317
317
  if (accRes.back) {
318
- // Cho phép quay về chọn folder dù ban đầu có --dir
318
+ // Allow returning to folder selection even when --dir was provided.
319
319
  delete eff.dir;
320
320
  step = 0;
321
321
  continue;
@@ -325,7 +325,7 @@ export async function runPushWizard(options = {}, onBackToMenu = null) {
325
325
  continue;
326
326
  }
327
327
 
328
- // BƯỚC 2: 📦 Chọn repository
328
+ // STEP 2: 📦 Choose repository.
329
329
  if (step === 2) {
330
330
  if (eff.repo) {
331
331
  repoUrl = eff.repo;
@@ -340,8 +340,8 @@ export async function runPushWizard(options = {}, onBackToMenu = null) {
340
340
  }
341
341
  repoUrl = pickedUrl;
342
342
  if (!repoUrl || !isValidGitUrl(repoUrl)) {
343
- printErrorBox('URL REPOSITORY KHÔNG HỢP LỆ', [`URL: '${repoUrl || '(trống)'}'`]);
344
- const retry = await promptContinueOrBack('URL chưa hợp lệ. Bạn muốn thử lại hay quay lại?');
343
+ printErrorBox('INVALID REPOSITORY URL', [`URL: '${repoUrl || '(empty)'}'`]);
344
+ const retry = await promptContinueOrBack('Invalid URL. Would you like to try again or go back?');
345
345
  if (retry === 'exit') { printCancelled(); process.exit(0); }
346
346
  if (retry === 'back') step = 1;
347
347
  continue;
@@ -350,7 +350,7 @@ export async function runPushWizard(options = {}, onBackToMenu = null) {
350
350
  continue;
351
351
  }
352
352
 
353
- // BƯỚC 3: 🌿 Chọn branch
353
+ // STEP 3: 🌿 Choose branch.
354
354
  if (step === 3) {
355
355
  if (eff.branch) {
356
356
  branchName = String(eff.branch).trim();
@@ -360,17 +360,17 @@ export async function runPushWizard(options = {}, onBackToMenu = null) {
360
360
  }
361
361
  const currentB = getCurrentBranch(targetDir);
362
362
  const val = await inputWithBack({
363
- message: 'Đặt tên nhánh Git',
364
- promptLabel: 'Tên nhánh',
363
+ message: 'Choose a Git branch name',
364
+ promptLabel: 'Branch name',
365
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.',
366
+ 'The local branch will be renamed before it is pushed to origin.',
367
+ 'Examples: main, dev, feature/login.',
368
368
  ],
369
369
  default: currentB || 'main',
370
370
  clearPromptOnDone: true,
371
371
  validate: (v) => {
372
372
  if (!v || !v.trim()) return true;
373
- return isValidBranchName(v.trim()) || 'Tên nhánh không hợp lệ: không dùng dấu cách, .., ~ ^ : ? * [ \\ hoặc @{.';
373
+ return isValidBranchName(v.trim()) || 'Invalid branch name. Avoid spaces, .., ~ ^ : ? * [ \\ and @{.';
374
374
  }
375
375
  });
376
376
  if (val === BACK) {
@@ -379,14 +379,14 @@ export async function runPushWizard(options = {}, onBackToMenu = null) {
379
379
  }
380
380
  branchName = String(val || currentB || 'main').trim();
381
381
  if (!isValidBranchName(branchName)) {
382
- printErrorBox('TÊN NHÁNH KHÔNG HỢP LỆ', [`Tên nhánh: '${branchName}'`]);
382
+ printErrorBox('INVALID BRANCH NAME', [`Branch: '${branchName}'`]);
383
383
  continue;
384
384
  }
385
385
  step = 4;
386
386
  continue;
387
387
  }
388
388
 
389
- // BƯỚC 4: 💬 Nhập commit message
389
+ // STEP 4: 💬 Enter commit message.
390
390
  if (step === 4) {
391
391
  if (eff.message) {
392
392
  commitMsg = String(eff.message).trim();
@@ -395,11 +395,11 @@ export async function runPushWizard(options = {}, onBackToMenu = null) {
395
395
  continue;
396
396
  }
397
397
  const val = await inputWithBack({
398
- message: 'Viết commit message',
399
- promptLabel: 'Commit',
398
+ message: 'Write a commit message',
399
+ promptLabel: 'Commit message',
400
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.',
401
+ 'Briefly describe the changes you are about to push.',
402
+ 'Examples: feat: add sign-in · fix: resolve push error.',
403
403
  ],
404
404
  default: 'Initial commit',
405
405
  clearPromptOnDone: true
@@ -413,7 +413,7 @@ export async function runPushWizard(options = {}, onBackToMenu = null) {
413
413
  continue;
414
414
  }
415
415
 
416
- // BƯỚC 5: ⏰ Chọn commit time
416
+ // STEP 5: ⏰ Choose commit time.
417
417
  if (step === 5) {
418
418
  if (eff.date) {
419
419
  dateInput = eff.date;
@@ -422,13 +422,13 @@ export async function runPushWizard(options = {}, onBackToMenu = null) {
422
422
  continue;
423
423
  }
424
424
  const dateOption = await selectWithBack({
425
- message: 'Bước 6/7 · Thời gian commit',
426
- hint: 'Chọn mốc thời gian ghi vào commit',
425
+ message: 'Step 6/7 · Commit time',
426
+ hint: 'Choose the timestamp to record in the commit',
427
427
  clearPromptOnDone: true,
428
428
  choices: [
429
- { name: 'Ngay bây giờ', value: 'now', description: 'Sử dụng thời gian hiện tại' },
430
- { name: 'Lùi thời gian (Backdate)', value: 'relative', description: 'VD: -2d (2 ngày trước), -5h, -30m, -1w' },
431
- { name: 'Ngày giờ cụ thể', value: 'custom', description: 'VD: 2024-01-15 14:30:00' }
429
+ { name: 'Now', value: 'now', description: 'Use the current time' },
430
+ { name: 'Relative time (backdate)', value: 'relative', description: 'e.g. -2d (2 days ago), -5h, -30m, -1w' },
431
+ { name: 'Specific date and time', value: 'custom', description: 'e.g. 2024-01-15 14:30:00' }
432
432
  ]
433
433
  });
434
434
  if (dateOption === BACK) {
@@ -440,21 +440,21 @@ export async function runPushWizard(options = {}, onBackToMenu = null) {
440
440
  dateInput = 'now';
441
441
  } else if (dateOption === 'relative') {
442
442
  const val = await inputWithBack({
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'],
443
+ message: 'Backdate the commit',
444
+ hint: ['-2d = 2 days ago · -5h = 5 hours ago · -30m = 30 minutes ago · -1w = 1 week ago'],
445
445
  default: '-1d',
446
446
  clearPromptOnDone: true,
447
- validate: (v) => { const p = parseCustomDate(v); return p.valid || p.error || 'Thời gian không hợp lệ'; }
447
+ validate: (v) => { const p = parseCustomDate(v); return p.valid || p.error || 'Invalid time'; }
448
448
  });
449
449
  if (val === BACK) continue;
450
450
  dateInput = val;
451
451
  } else if (dateOption === 'custom') {
452
452
  const val = await inputWithBack({
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)'],
453
+ message: 'Set a specific commit date and time',
454
+ hint: ['Format: YYYY-MM-DD HH:mm:ss (e.g. 2024-01-15 10:00:00)'],
455
455
  default: '2024-01-15 10:00:00',
456
456
  clearPromptOnDone: true,
457
- validate: (v) => { const p = parseCustomDate(v); return p.valid || p.error || 'Ngày giờ không hợp lệ'; }
457
+ validate: (v) => { const p = parseCustomDate(v); return p.valid || p.error || 'Invalid date and time'; }
458
458
  });
459
459
  if (val === BACK) continue;
460
460
  dateInput = val;
@@ -462,8 +462,8 @@ export async function runPushWizard(options = {}, onBackToMenu = null) {
462
462
 
463
463
  parsedDate = parseCustomDate(dateInput || 'now');
464
464
  if (!parsedDate.valid) {
465
- printErrorBox('THỜI GIAN KHÔNG HỢP LỆ', [parsedDate.error]);
466
- const retry = await promptContinueOrBack('Thời gian chưa đúng. Bạn muốn nhập lại hay quay lại?');
465
+ printErrorBox('INVALID DATE OR TIME', [parsedDate.error]);
466
+ const retry = await promptContinueOrBack('Invalid time. Would you like to try again or go back?');
467
467
  if (retry === 'exit') { printCancelled(); process.exit(0); }
468
468
  dateInput = undefined;
469
469
  delete eff.date;
@@ -474,7 +474,7 @@ export async function runPushWizard(options = {}, onBackToMenu = null) {
474
474
  continue;
475
475
  }
476
476
 
477
- // BƯỚC 6: 🚀 Xác nhận & Đẩy lên
477
+ // STEP 6: 🚀 Review and push.
478
478
  if (step === 6) {
479
479
  if (!commitMsg) commitMsg = 'Initial commit';
480
480
  if (!dateInput) dateInput = 'now';
@@ -491,18 +491,18 @@ export async function runPushWizard(options = {}, onBackToMenu = null) {
491
491
  });
492
492
 
493
493
  const decision = await selectBox({
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',
494
+ message: 'Ready to push this project to Git?',
495
+ hint: 'Review the summary above before pushing',
496
496
  choices: [
497
- { name: 'Bắt đầu đẩy code (Push)', value: 'continue', description: 'Thực thi git init/add/commit/push' },
498
- { name: 'Sửa thời gian commit', value: 'back-date', description: 'Về bước chọn thời gian' },
499
- { name: 'Sửa commit message', value: 'back-msg', description: 'Về bước nhập message' },
500
- { name: 'Sửa branch', value: 'back-branch', description: 'Về bước chọn branch' },
501
- { name: 'Sửa repository', value: 'back-repo', description: 'Về bước chọn repository' },
502
- { name: 'Sửa tài khoản GitHub', value: 'back-account', description: 'Về bước chọn tài khoản GitHub' },
503
- { name: 'Chọn lại folder dự án', value: 'back-folder', description: 'Về bước chọn thư mục' },
504
- { name: 'Về menu chính', value: 'back-menu', description: 'Quay lại màn hình điều khiển' },
505
- { name: 'x Hủy bỏ', value: 'exit', description: 'Thoát' }
497
+ { name: 'Push project', value: 'continue', description: 'Run git init/add/commit/push' },
498
+ { name: 'Change commit time', value: 'back-date', description: 'Return to the time step' },
499
+ { name: 'Edit commit message', value: 'back-msg', description: 'Return to the message step' },
500
+ { name: 'Change branch', value: 'back-branch', description: 'Return to the branch step' },
501
+ { name: 'Change repository', value: 'back-repo', description: 'Return to the repository step' },
502
+ { name: 'Change GitHub account', value: 'back-account', description: 'Return to the account step' },
503
+ { name: 'Choose another project folder', value: 'back-folder', description: 'Return to the folder step' },
504
+ { name: 'Back to dashboard', value: 'back-menu', description: 'Return to the main menu' },
505
+ { name: 'Cancel', value: 'exit', description: 'Exit without continuing' }
506
506
  ]
507
507
  });
508
508
 
@@ -523,36 +523,36 @@ export async function runPushWizard(options = {}, onBackToMenu = null) {
523
523
  }
524
524
  }
525
525
 
526
- // 🚀 BƯỚC 7: THỰC THI PUSH
526
+ // 🚀 STEP 7: PERFORM THE PUSH.
527
527
  const spinner = ora({ color: 'cyan' });
528
- let secretStatus = '✓ Không phát hiện';
528
+ let secretStatus = '✓ None found';
529
529
  let pushDurationSeconds = null;
530
- printSection(`🚀 ĐANG ĐẨY CODE LÊN GIT (${TOTAL_STEPS} BƯỚC)`);
530
+ printSection(`🚀 PUSHING TO GIT (${TOTAL_STEPS} STEPS)`);
531
531
 
532
532
  try {
533
533
  // 1. Git init
534
534
  if (!isGitRepo(targetDir)) {
535
- spinner.start(stepLabel(1, TOTAL_STEPS, 'Khởi tạo Git repo (git init)...'));
535
+ spinner.start(stepLabel(1, TOTAL_STEPS, 'Initialize Git repository (git init)...'));
536
536
  initGitRepo(targetDir);
537
- spinner.succeed(stepLabel(1, TOTAL_STEPS, chalk.hex('#34D399')('Đã khởi tạo Git repo')));
537
+ spinner.succeed(stepLabel(1, TOTAL_STEPS, chalk.hex('#34D399')('Git repository initialized')));
538
538
  } else {
539
- console.log(' ' + stepLabel(1, TOTAL_STEPS, chalk.hex('#64748B')('Đã có Git repo sẵn — bỏ qua init')));
539
+ console.log(' ' + stepLabel(1, TOTAL_STEPS, chalk.hex('#64748B')('Git repository already exists — skipping init')));
540
540
  }
541
541
 
542
542
  // 2. Remote
543
- spinner.start(stepLabel(2, TOTAL_STEPS, 'Cấu hình remote origin...'));
543
+ spinner.start(stepLabel(2, TOTAL_STEPS, 'Configure origin remote...'));
544
544
  setGitRemote(repoUrl, targetDir);
545
- spinner.succeed(stepLabel(2, TOTAL_STEPS, chalk.hex('#34D399')('Cấu hình remote origin hoàn tất')));
545
+ spinner.succeed(stepLabel(2, TOTAL_STEPS, chalk.hex('#34D399')('Origin remote configured')));
546
546
 
547
547
  // 3. Stage
548
548
  spinner.start(stepLabel(3, TOTAL_STEPS, 'Stage files (git add .)...'));
549
549
  stageAllFiles(targetDir);
550
- spinner.succeed(stepLabel(3, TOTAL_STEPS, chalk.hex('#34D399')('Đã stage toàn bộ files')));
550
+ spinner.succeed(stepLabel(3, TOTAL_STEPS, chalk.hex('#34D399')('All files staged')));
551
551
 
552
- // 3.5. Quét secret nhạy cảm
552
+ // 3.5. Scan for secrets.
553
553
  const risky = findRiskyStagedFiles(targetDir);
554
554
  const findings = scanStagedContentForSecrets(targetDir);
555
- if (risky.length > 0 || findings.length > 0) secretStatus = `! Có ${risky.length + findings.length} cảnh báo`;
555
+ if (risky.length > 0 || findings.length > 0) secretStatus = `! ${risky.length + findings.length} warning(s)`;
556
556
  if (risky.length > 0 || findings.length > 0) {
557
557
  spinner.stop();
558
558
  printPrePushSecretWarning({ riskyFiles: risky, findings });
@@ -561,10 +561,10 @@ export async function runPushWizard(options = {}, onBackToMenu = null) {
561
561
  let wantFix = options.fix === true;
562
562
  if (!wantFix && isInteractive) {
563
563
  const fixChoice = await selectWithBack({
564
- message: 'Tự động gỡ file secret an toàn? (giữ nguyên file ở máy, gỡ khỏi commit)',
564
+ message: 'Safely remove the secret files? (keeps local files, removes them from the commit)',
565
565
  choices: [
566
- { name: 'Có, tự động gỡ file nhạy cảm', value: 'yes', description: 'Gỡ file secret khỏi stage & thêm .gitignore' },
567
- { name: 'Không, giữ nguyên', value: 'no', description: 'Tiếp tục mà không gỡ' }
566
+ { name: 'Yes, remove sensitive files', value: 'yes', description: 'Unstage secret files and add them to .gitignore' },
567
+ { name: 'No, keep them', value: 'no', description: 'Continue without removing them' }
568
568
  ]
569
569
  });
570
570
  if (fixChoice === BACK) {
@@ -575,51 +575,51 @@ export async function runPushWizard(options = {}, onBackToMenu = null) {
575
575
  }
576
576
 
577
577
  if (wantFix) {
578
- spinner.start('Đang tự fix: gỡ file secret khỏi stage...');
578
+ spinner.start('Removing secret files from the staged changes...');
579
579
  const res = autoFixStagedSecrets(allFiles, targetDir);
580
- spinner.succeed(chalk.hex('#34D399')(`[OK] Đã gỡ ${res.fixed.length} file khỏi commit`));
580
+ spinner.succeed(chalk.hex('#34D399')(`[OK] Removed ${res.fixed.length} file(s) from the commit`));
581
581
  printFixResult(res);
582
582
 
583
583
  const remainingRisky = findRiskyStagedFiles(targetDir);
584
584
  const remainingFindings = scanStagedContentForSecrets(targetDir);
585
585
  secretStatus = remainingRisky.length || remainingFindings.length
586
- ? `! Còn ${remainingRisky.length + remainingFindings.length} cảnh báo`
587
- : '✓ Đã gỡ cảnh báo';
586
+ ? `! ${remainingRisky.length + remainingFindings.length} warning(s) remain`
587
+ : '✓ Warnings removed';
588
588
 
589
589
  spawnSync('git', ['add', '.gitignore'], { cwd: targetDir, stdio: 'ignore' });
590
590
 
591
591
  if (!hasStagedChanges(targetDir)) {
592
- printErrorBox('KHÔNG CÒN FILE ĐỂ COMMIT', [
593
- 'Sau khi gỡ secret thì không còn file nào để commit.',
594
- 'File của bạn vẫn còn trên máy, không bị mất.'
592
+ printErrorBox('NOTHING LEFT TO COMMIT', [
593
+ 'No files remain to commit after removing the secrets.',
594
+ 'Your local files are still intact.'
595
595
  ]);
596
596
  if (fromMenu) return 'back';
597
597
  process.exit(0);
598
598
  }
599
- console.log(chalk.hex('#34D399')(' » Tiếp tục commit/push phần code an toàn.\n'));
599
+ console.log(chalk.hex('#34D399')(' » Continuing with the safe changes.\n'));
600
600
  }
601
601
  }
602
602
 
603
603
  // 4. Commit
604
- spinner.start(stepLabel(4, TOTAL_STEPS, `Tạo commit [${parsedDate.display}]...`));
604
+ spinner.start(stepLabel(4, TOTAL_STEPS, `Create commit [${parsedDate.display}]...`));
605
605
  if (!hasChangesToCommit(targetDir)) {
606
- spinner.info(stepLabel(4, TOTAL_STEPS, chalk.hex('#94A3B8')('Không có thay đổi mới — dùng commit hiện có')));
606
+ spinner.info(stepLabel(4, TOTAL_STEPS, chalk.hex('#94A3B8')('No new changes — using the existing commit')));
607
607
  } else {
608
608
  commitWithCustomDate(commitMsg, parsedDate.formatted, targetDir);
609
- spinner.succeed(stepLabel(4, TOTAL_STEPS, chalk.hex('#34D399')(`Commit hoàn tất • ${chalk.hex('#C084FC').bold(parsedDate.display)}`)));
609
+ spinner.succeed(stepLabel(4, TOTAL_STEPS, chalk.hex('#34D399')(`Commit created • ${chalk.hex('#C084FC').bold(parsedDate.display)}`)));
610
610
  }
611
611
 
612
612
  // 5. Branch
613
- spinner.start(stepLabel(5, TOTAL_STEPS, `Đổi branch -> '${branchName}'...`));
613
+ spinner.start(stepLabel(5, TOTAL_STEPS, `Switch branch -> '${branchName}'...`));
614
614
  setBranchName(branchName, targetDir);
615
615
  spinner.succeed(stepLabel(5, TOTAL_STEPS, chalk.hex('#34D399')(`Branch: ${chalk.hex('#5EEAD4').bold(branchName)}`)));
616
616
 
617
617
  // 6. Push
618
- spinner.start(stepLabel(6, TOTAL_STEPS, `Push lên origin/${branchName}...`));
618
+ spinner.start(stepLabel(6, TOTAL_STEPS, `Push to origin/${branchName}...`));
619
619
  const pushStartedAt = Date.now();
620
620
  pushToRemote(branchName, options.force, targetDir);
621
621
  pushDurationSeconds = (Date.now() - pushStartedAt) / 1000;
622
- spinner.succeed(stepLabel(6, TOTAL_STEPS, chalk.bold.hex('#34D399')('Push lên remote thành công!')));
622
+ spinner.succeed(stepLabel(6, TOTAL_STEPS, chalk.bold.hex('#34D399')('Successfully pushed to remote')));
623
623
 
624
624
  printSuccessBox({ branch: branchName, repoUrl, durationSeconds: pushDurationSeconds, secretStatus });
625
625
  printFooter();
@@ -630,37 +630,37 @@ export async function runPushWizard(options = {}, onBackToMenu = null) {
630
630
 
631
631
  const unpushed = getUnpushedCommits(branchName, targetDir);
632
632
  if (unpushed.length > 0) {
633
- console.log(chalk.cyanBright(`\n » Phát hiện bạn có ${unpushed.length} commit chưa push có thể chứa vết secret cũ.`));
633
+ console.log(chalk.cyanBright(`\n » Found ${unpushed.length} unpushed commit(s) that may contain traces of an old secret.`));
634
634
  let doSquash = options.fix === true;
635
635
  if (!doSquash && isInteractive) {
636
636
  const sqChoice = await selectWithBack({
637
- message: 'Tự động làm sạch lịch sử (squash các commit thành 1 commit sạch) để push lại?',
637
+ message: 'Clean the history (squash commits into one clean commit) and try pushing again?',
638
638
  choices: [
639
- { name: 'Có, squash và push lại ngay', value: 'yes', description: 'Làm sạch commit' },
640
- { name: 'Không, để tự xử lý', value: 'no', description: 'Dừng' }
639
+ { name: 'Yes, squash and retry the push', value: 'yes', description: 'Clean the commit history' },
640
+ { name: 'No, I will handle it', value: 'no', description: 'Stop here' }
641
641
  ]
642
642
  });
643
643
  doSquash = sqChoice === 'yes';
644
644
  }
645
645
  if (doSquash) {
646
- spinner.start('Đang squash và làm sạch commit chưa push...');
646
+ spinner.start('Squashing and cleaning unpushed commits...');
647
647
  const sqRes = squashUnpushedCommits(branchName, commitMsg, parsedDate.formatted, targetDir);
648
648
  if (sqRes.ok) {
649
- spinner.succeed(chalk.green('[OK] Đã làm sạch commit! Đang thử push lại...'));
649
+ spinner.succeed(chalk.green('[OK] Commit history cleaned. Retrying push...'));
650
650
  try {
651
- spinner.start(stepLabel(6, TOTAL_STEPS, `Push lại lên origin/${branchName}...`));
651
+ spinner.start(stepLabel(6, TOTAL_STEPS, `Retry push to origin/${branchName}...`));
652
652
  const retryStartedAt = Date.now();
653
653
  pushToRemote(branchName, options.force, targetDir);
654
654
  pushDurationSeconds = (Date.now() - retryStartedAt) / 1000;
655
- spinner.succeed(chalk.bold.green('[OK] Push thành công'));
655
+ spinner.succeed(chalk.bold.green('[OK] Push succeeded'));
656
656
  printSuccessBox({ branch: branchName, repoUrl, durationSeconds: pushDurationSeconds, secretStatus });
657
657
  printFooter();
658
658
  return;
659
659
  } catch (retryErr) {
660
- spinner.fail(chalk.red(`Push lại thất bại: ${retryErr.message}`));
660
+ spinner.fail(chalk.red(`Retry push failed: ${retryErr.message}`));
661
661
  }
662
662
  } else {
663
- spinner.fail(chalk.red(`Không thể squash: ${sqRes.error}`));
663
+ spinner.fail(chalk.red(`Unable to squash commits: ${sqRes.error}`));
664
664
  }
665
665
  }
666
666
  }
@@ -670,37 +670,37 @@ export async function runPushWizard(options = {}, onBackToMenu = null) {
670
670
  printNonFastForwardBox(error.message, branchName);
671
671
  if (isInteractive) {
672
672
  const ffChoice = await selectBox({
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`,
673
+ message: 'The remote has newer commits. What would you like to do?',
674
+ hint: `git pull --rebase origin ${branchName}, then push again`,
675
675
  choices: [
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` },
677
- { name: 'Chỉ Pull về (chưa push vội)', value: 'pull-only', description: 'Chỉ đồng bộ code về máy' },
678
- { name: 'Quay lại menu chính', value: 'back', description: 'Về màn hình điều khiển' },
679
- { name: 'x Thoát', value: 'exit', description: 'Dừng lại' }
676
+ { name: 'Pull with rebase, then push — Recommended', value: 'pull-push', description: `git pull --rebase origin ${branchName}, then push again` },
677
+ { name: 'Pull only', value: 'pull-only', description: 'Sync the latest changes without pushing' },
678
+ { name: 'Back to dashboard', value: 'back', description: 'Return to the main menu' },
679
+ { name: 'Exit', value: 'exit', description: 'Stop here' }
680
680
  ]
681
681
  });
682
682
  if (ffChoice === 'pull-push' || ffChoice === 'pull-only') {
683
683
  try {
684
- spinner.start(`Đang pull --rebase origin/${branchName}...`);
684
+ spinner.start(`Pulling --rebase from origin/${branchName}...`);
685
685
  const pullOut = pullFromRemote('origin', branchName, targetDir, { rebase: true });
686
- spinner.succeed(chalk.hex('#34D399')(`Đã pull --rebase origin/${branchName} xong`));
686
+ spinner.succeed(chalk.hex('#34D399')(`Pulled --rebase from origin/${branchName}`));
687
687
  if (pullOut) console.log(chalk.hex('#64748B')(` ${String(pullOut).split('\n')[0].slice(0, 120)}`));
688
688
  if (ffChoice === 'pull-push') {
689
- spinner.start(stepLabel(6, TOTAL_STEPS, `Push lại lên origin/${branchName}...`));
689
+ spinner.start(stepLabel(6, TOTAL_STEPS, `Retry push to origin/${branchName}...`));
690
690
  const retryStartedAt = Date.now();
691
691
  pushToRemote(branchName, options.force, targetDir);
692
692
  pushDurationSeconds = (Date.now() - retryStartedAt) / 1000;
693
- spinner.succeed(chalk.bold.hex('#34D399')('Push lại thành công sau khi pull!'));
693
+ spinner.succeed(chalk.bold.hex('#34D399')('Push succeeded after pulling'));
694
694
  printSuccessBox({ branch: branchName, repoUrl, durationSeconds: pushDurationSeconds, secretStatus });
695
695
  printFooter();
696
696
  return;
697
697
  }
698
- console.log(chalk.hex('#34D399')('\n [OK] Đã pull code mới nhất về.\n'));
698
+ console.log(chalk.hex('#34D399')('\n [OK] Pulled the latest changes.\n'));
699
699
  if (fromMenu) return 'back';
700
700
  return;
701
701
  } catch (pullErr) {
702
- spinner.fail(chalk.red(`Pull --rebase thất bại: ${String(pullErr.message).split('\n')[0]}`));
703
- printErrorBox('PULL --REBASE THẤT BẠI', [String(pullErr.message).split('\n')[0]]);
702
+ spinner.fail(chalk.red(`Pull --rebase failed: ${String(pullErr.message).split('\n')[0]}`));
703
+ printErrorBox('PULL --REBASE FAILED', [String(pullErr.message).split('\n')[0]]);
704
704
  if (fromMenu) return 'back';
705
705
  return;
706
706
  }
@@ -709,11 +709,11 @@ export async function runPushWizard(options = {}, onBackToMenu = null) {
709
709
  }
710
710
  }
711
711
  } else if (error.code === 'REMOTE_OR_NETWORK') {
712
- printErrorBox('LỖI KẾT NỐI REMOTE', [String(error.message).split('\n')[0]], 'Kiểm tra URL repo, mạng, và quyền truy cập.');
712
+ printErrorBox('REMOTE CONNECTION ERROR', [String(error.message).split('\n')[0]], 'Check the repository URL, network connection, and access permissions.');
713
713
  } else {
714
- printErrorBox('ĐẨY LÊN GIT THẤT BẠI', [error.message]);
714
+ printErrorBox('GIT PUSH FAILED', [error.message]);
715
715
  }
716
- // Khi chạy từ menu tương tác: quay về menu thay vì kill process
716
+ // When running from the interactive menu, return to it instead of exiting.
717
717
  if (fromMenu) return 'back';
718
718
  process.exit(1);
719
719
  }