git-push-time 1.0.0 → 1.0.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.
package/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # 🚀 GitHub CLI (`githubcli`)
1
+ # 🚀 Git Push Time / GitHub CLI (`git-push-time`)
2
2
 
3
3
  Một công cụ Dòng lệnh (CLI) mạnh mẽ và tiện lợi giúp bạn **đẩy dự án lên Git (GitHub, GitLab, Bitbucket,...) cực nhanh** chỉ bằng cách dán URL repo, đồng thời hỗ trợ **tùy chỉnh hoặc lùi thời gian commit/push** (Author Date & Committer Date).
4
4
 
@@ -16,17 +16,17 @@ Một công cụ Dòng lệnh (CLI) mạnh mẽ và tiện lợi giúp bạn **
16
16
 
17
17
  ---
18
18
 
19
- ## 📦 Cài đặt
19
+ ## 📦 Cài đặt từ NPM
20
20
 
21
21
  ### Cách 1: Cài đặt toàn cục (Recommended)
22
22
  ```bash
23
- npm install -g githubcli
23
+ npm install -g git-push-time
24
24
  ```
25
- Sau khi cài đặt, bạn có thể gọi lệnh `githubcli`, `git-push-time` hoặc `git-custom-push` ở **bất kỳ thư mục dự án nào** trên máy tính!
25
+ Sau khi cài đặt, bạn có thể gọi lệnh `git-push-time`, `githubcli` hoặc `git-custom-push` ở **bất kỳ thư mục dự án nào** trên máy tính!
26
26
 
27
- ### Cách 2: Chạy trực tiếp không cần cài đặt
27
+ ### Cách 2: Chạy trực tiếp qua `npx`
28
28
  ```bash
29
- npx githubcli
29
+ npx git-push-time
30
30
  ```
31
31
 
32
32
  ---
@@ -85,16 +85,26 @@ git-push-time -r https://github.com/username/my-repo.git -d "-5h" -m "Update doc
85
85
 
86
86
  ---
87
87
 
88
- ## 🚩 Danh sách các Flags (Options)
89
-
90
88
  - `-r, --repo <url>`: Link Git Repository (VD: `https://github.com/user/repo.git`)
91
89
  - `-d, --date <datetime>`: Thời gian commit (`now`, `-2d`, `2024-01-15 10:30:00`,...)
92
90
  - `-m, --message <msg>`: Thông điệp commit (Mặc định: `Initial commit`)
93
91
  - `-b, --branch <branch>`: Tên branch (Mặc định: `main`)
94
92
  - `-f, --force`: Đẩy đè lên branch remote (`--force`)
95
93
  - `-y, --yes`: Tự động xác nhận, không hiện prompt chọn
94
+ - `--fix`: Tự động gỡ file secret (.env, .npmrc...) khỏi stage và thêm vào `.gitignore`
95
+ - `--publish`: Publish package lên npm sau khi push Git thành công
96
96
  - `-h, --help`: Hướng dẫn trợ giúp
97
97
 
98
+ ### 🛠️ Lệnh Fix Secret Độc Lập
99
+ Nếu phát hiện repo bị dính secret hoặc GitHub Push Protection (GH013) chặn push, bạn có thể chạy ngay lệnh fix:
100
+ ```bash
101
+ git-push-time fix
102
+ ```
103
+ Lệnh sẽ tự động:
104
+ 1. Quét vùng stage và thư mục làm việc để tìm file cấu hình nhạy cảm (`.env`, `.npmrc`, keys...).
105
+ 2. Gỡ file khỏi stage và thêm rule an toàn vào `.gitignore`.
106
+ 3. Kiểm tra các commit chưa push (unpushed) dính secret cũ và hỗ trợ tự động squash thành commit sạch để GitHub cho phép push.
107
+
98
108
  ---
99
109
 
100
110
  ## 📜 Giấy phép
package/bin/index.js CHANGED
@@ -15,31 +15,53 @@ import {
15
15
  commitWithCustomDate,
16
16
  setBranchName,
17
17
  pushToRemote,
18
- getCurrentBranch
18
+ getCurrentBranch,
19
+ findRiskyStagedFiles
19
20
  } from '../lib/git.js';
21
+ import { scanStagedContentForSecrets } from '../lib/secrets.js';
22
+ import {
23
+ autoFixStagedSecrets,
24
+ hasStagedChanges,
25
+ findRiskyWorkingTreeFiles,
26
+ getUnpushedCommits,
27
+ squashUnpushedCommits
28
+ } from '../lib/fix.js';
29
+ import {
30
+ checkNpmInstalled,
31
+ getPackageInfo,
32
+ checkNpmLogin,
33
+ isNpmrcTracked,
34
+ publishToNpm
35
+ } from '../lib/npm.js';
36
+ import {
37
+ printBanner,
38
+ printSummary,
39
+ printSection,
40
+ stepLabel,
41
+ printPrePushSecretWarning,
42
+ printSecretBlockedBox,
43
+ printSuccessBox,
44
+ printCancelled,
45
+ printErrorBox,
46
+ printGitMissing,
47
+ printAuthFailedBox,
48
+ printNonFastForwardBox,
49
+ printFixResult,
50
+ printNpmResult,
51
+ printFooter,
52
+ styleCliHelp
53
+ } from '../lib/ui.js';
20
54
 
21
- const program = new Command();
22
-
23
- program
24
- .name('githubcli')
25
- .description('Công cụ CLI giúp đẩy dự án lên Git và sửa thời gian commit (Backdate Commit)')
26
- .version('1.0.0')
27
- .option('-r, --repo <url>', 'Đường dẫn Git repository (URL)')
28
- .option('-d, --date <datetime>', 'Thời gian commit (Ví dụ: "2024-01-15 14:30:00", "-2d", "-5h", "now")')
29
- .option('-m, --message <msg>', 'Thông điệp commit (Commit message)')
30
- .option('-b, --branch <branch>', 'Tên branch trên remote (Mặc định: main)')
31
- .option('-f, --force', 'Thực hiện push đè (--force)', false)
32
- .option('-y, --yes', 'Bỏ qua prompt tương tác, tự động dùng tham số hoặc giá trị mặc định', false);
55
+ const VERSION = '1.0.2';
56
+ const TOTAL_STEPS = 6;
33
57
 
34
- program.parse(process.argv);
35
- const options = program.opts();
58
+ async function run(options = {}) {
59
+ printBanner(VERSION);
36
60
 
37
- async function run() {
38
- console.log(chalk.bold.cyan('\n🚀 GIT PUSH & COMMIT TIME SPOOFER 🚀\n'));
39
-
40
- // 1. Kiểm tra Git đã cài đặt chưa
61
+ // 1. Kiểm tra Git
41
62
  if (!checkGitInstalled()) {
42
- console.error(chalk.red('❌ Hệ thống chưa cài đặt Git. Vui lòng cài đặt Git trước khi dùng công cụ này.'));
63
+ printBanner(VERSION);
64
+ printGitMissing();
43
65
  process.exit(1);
44
66
  }
45
67
 
@@ -50,36 +72,36 @@ async function run() {
50
72
 
51
73
  const isInteractive = !options.yes;
52
74
 
53
- // 2. Chế độ Tương tác (Interactive Prompts) nếu thiếu tham số
75
+ // 2. Thu thập thông tin
54
76
  if (isInteractive) {
55
- // Nhập URL Repo nếu chưa truyền qua CLI
77
+ printSection('🔗 KẾT NỐI REPOSITORY');
78
+
56
79
  if (!repoUrl) {
57
80
  repoUrl = await input({
58
- message: 'Dán link Git Repository (Ví dụ: https://github.com/user/repo.git):',
81
+ message: '📦 Dán link Git Repository:',
59
82
  validate: (value) => {
60
83
  if (!value || !value.trim()) return 'Vui lòng nhập đường dẫn repository!';
61
- if (!isValidGitUrl(value)) return 'Đường dẫn Git URL không đúng định dạng (https/ssh/.git)';
84
+ if (!isValidGitUrl(value)) return 'URL chưa đúng (vd: https://github.com/user/repo.git)';
62
85
  return true;
63
86
  }
64
87
  });
65
88
  }
66
89
 
67
- // Nhập Commit Message nếu chưa truyền
68
90
  if (!commitMsg) {
69
91
  commitMsg = await input({
70
- message: 'Nhập thông điệp Commit (Commit message):',
92
+ message: '💬 Thông điệp commit:',
71
93
  default: 'Initial commit'
72
94
  });
73
95
  }
74
96
 
75
- // Chọn cách nhập thời gian Commit nếu chưa truyền
76
97
  if (!dateInput) {
98
+ printSection('⏰ THỜI GIAN COMMIT');
77
99
  const dateOption = await select({
78
- message: 'Chọn thời gian cho Commit:',
100
+ message: 'Chọn thời gian cho commit:',
79
101
  choices: [
80
- { name: '⏰ Ngay bây giờ (Current Time)', value: 'now' },
81
- { name: '⏳ Lùi thời gian (Ví dụ: -2d là 2 ngày trước, -5h là 5 giờ trước, -1w là 1 tuần trước)', value: 'relative' },
82
- { name: '📅 Nhập ngày giờ cụ thể (Ví dụ: 2024-01-15 14:30:00)', value: 'custom' }
102
+ { name: '⏰ Ngay bây giờ', value: 'now', description: 'Dùng thời gian hiện tại' },
103
+ { name: '⏳ Lùi thời gian', value: 'relative', description: 'VD: -2d, -5h, -30m, -1w' },
104
+ { name: '📅 Ngày giờ cụ thể', value: 'custom', description: 'VD: 2024-01-15 14:30:00' }
83
105
  ]
84
106
  });
85
107
 
@@ -87,7 +109,7 @@ async function run() {
87
109
  dateInput = 'now';
88
110
  } else if (dateOption === 'relative') {
89
111
  dateInput = await input({
90
- message: 'Nhập thời gian lùi (VD: -2d [2 ngày trước], -5h [5 giờ trước], -1w [1 tuần trước]):',
112
+ message: '⏳ Nhập thời gian lùi (VD: -2d, -5h, -30m, -1w):',
91
113
  default: '-1d',
92
114
  validate: (val) => {
93
115
  const parsed = parseCustomDate(val);
@@ -96,7 +118,7 @@ async function run() {
96
118
  });
97
119
  } else if (dateOption === 'custom') {
98
120
  dateInput = await input({
99
- message: 'Nhập ngày giờ cụ thể (Định dạng: YYYY-MM-DD HH:mm:ss):',
121
+ message: '📅 Nhập ngày giờ (YYYY-MM-DD HH:mm:ss):',
100
122
  default: '2024-01-15 10:00:00',
101
123
  validate: (val) => {
102
124
  const parsed = parseCustomDate(val);
@@ -106,105 +128,376 @@ async function run() {
106
128
  }
107
129
  }
108
130
 
109
- // Nhập Branch Name nếu chưa truyền và chưa đổi
110
131
  if (!options.branch) {
111
132
  const currentB = getCurrentBranch();
112
133
  branchName = await input({
113
- message: 'Tên branch trên remote:',
134
+ message: '🌿 Branch trên remote:',
114
135
  default: currentB || 'main'
115
136
  });
116
137
  }
117
138
  }
118
139
 
119
- // Set default fallback values
120
140
  if (!commitMsg) commitMsg = 'Initial commit';
121
141
  if (!dateInput) dateInput = 'now';
122
142
  if (!branchName) branchName = 'main';
123
143
 
124
- // Validate Git URL
125
144
  if (!repoUrl || !isValidGitUrl(repoUrl)) {
126
- console.error(chalk.red(`❌ Lỗi: URL repository '${repoUrl}' không hợp lệ!`));
127
- console.log(chalk.yellow('Ví dụ URL đúng: https://github.com/username/repository.git'));
145
+ printErrorBox('URL REPOSITORY KHÔNG HỢP LỆ', [
146
+ `URL nhận được: '${repoUrl || '(trống)'}'`,
147
+ ], 'Ví dụ đúng: https://github.com/username/repository.git');
128
148
  process.exit(1);
129
149
  }
130
150
 
131
- // Parse Date/Time
132
151
  const parsedDate = parseCustomDate(dateInput);
133
152
  if (!parsedDate.valid) {
134
- console.error(chalk.red(`❌ Lỗi: ${parsedDate.error}`));
153
+ printErrorBox('THỜI GIAN KHÔNG HỢP LỆ', [parsedDate.error], 'VD: now, -2d, -5h, 2024-01-15 14:30:00');
135
154
  process.exit(1);
136
155
  }
137
156
 
138
- // Hiển thị tóm tắt thông tin trước khi thực thi
139
- console.log('\n' + chalk.bgBlue.white.bold(' 📋 THÔNG TIN ĐẨY DỰ ÁN '));
140
- console.log(chalk.gray('----------------------------------------'));
141
- console.log(`🔗 Repo Remote : ${chalk.green(repoUrl)}`);
142
- console.log(`💬 Commit Msg : ${chalk.yellow(commitMsg)}`);
143
- console.log(`📅 Commit Time : ${chalk.magenta(parsedDate.display)}`);
144
- console.log(`🌿 Branch : ${chalk.cyan(branchName)}`);
145
- console.log(`⚡ Force Push : ${options.force ? chalk.red('CÓ (--force)') : chalk.gray('Không')}`);
146
- console.log(chalk.gray('----------------------------------------\n'));
157
+ // Tóm tắt
158
+ printSummary({
159
+ repoUrl,
160
+ commitMsg,
161
+ dateDisplay: parsedDate.display,
162
+ branch: branchName,
163
+ force: options.force
164
+ });
147
165
 
148
166
  if (isInteractive) {
149
167
  const ok = await confirm({
150
- message: 'Xác nhận thực hiện tạo commit và đẩy lên Git?',
168
+ message: '🚀 Xác nhận đẩy lên Git ngay?',
151
169
  default: true
152
170
  });
153
171
  if (!ok) {
154
- console.log(chalk.yellow('Đã hủy thao tác.'));
172
+ printCancelled();
155
173
  process.exit(0);
156
174
  }
157
175
  }
158
176
 
159
- // 3. Thực thi các bước đẩy dự án
160
- const spinner = ora();
177
+ // 3. Thực thi
178
+ const spinner = ora({ color: 'cyan' });
179
+ printSection(`⚙️ ĐANG TRIỂN KHAI (${TOTAL_STEPS} bước)`);
161
180
 
162
181
  try {
163
- // Bước 1: Git Init
182
+ // [1/6] Git init
164
183
  if (!isGitRepo()) {
165
- spinner.start('Đang khởi tạo kho chứa Git (git init)...');
184
+ spinner.start(stepLabel(1, TOTAL_STEPS, 'Khởi tạo Git repo (git init)…'));
166
185
  initGitRepo();
167
- spinner.succeed('Đã khởi tạo kho chứa Git cục bộ.');
186
+ spinner.succeed(stepLabel(1, TOTAL_STEPS, chalk.hex('#34D399')('Đã khởi tạo Git repo')));
168
187
  } else {
169
- spinner.info('Đã phát hiện kho chứa Git có sẵn.');
188
+ console.log(' ' + stepLabel(1, TOTAL_STEPS, chalk.hex('#64748B')('Đã có Git repo sẵn — bỏ qua init')));
170
189
  }
171
190
 
172
- // Bước 2: Thiết lập Remote URL
173
- spinner.start(`Đang cấu hình Remote Origin: ${repoUrl}`);
191
+ // [2/6] Remote
192
+ spinner.start(stepLabel(2, TOTAL_STEPS, 'Cấu hình remote origin…'));
174
193
  setGitRemote(repoUrl);
175
- spinner.succeed(`Đã cấu hình Remote Origin thành công.`);
194
+ spinner.succeed(stepLabel(2, TOTAL_STEPS, chalk.hex('#34D399')('Cấu hình remote origin hoàn tất')));
176
195
 
177
- // Bước 3: Stage files (git add .)
178
- spinner.start('Đang thêm tất cả các file (git add .)...');
196
+ // [3/6] Stage
197
+ spinner.start(stepLabel(3, TOTAL_STEPS, 'Stage files (git add .)…'));
179
198
  stageAllFiles();
180
- spinner.succeed('Đã stage tất cả các file.');
199
+ spinner.succeed(stepLabel(3, TOTAL_STEPS, chalk.hex('#34D399')('Đã stage toàn bộ files')));
200
+
201
+ // [3.5] Quét secret: tên file + nội dung
202
+ const risky = findRiskyStagedFiles();
203
+ const findings = scanStagedContentForSecrets();
204
+ if (risky.length > 0 || findings.length > 0) {
205
+ spinner.stop();
206
+ printPrePushSecretWarning({ riskyFiles: risky, findings });
207
+ const allFiles = [...new Set([...risky, ...findings.map((f) => f.file)])];
208
+
209
+ let wantFix = options.fix === true;
210
+ if (!wantFix && isInteractive) {
211
+ wantFix = await confirm({
212
+ message: `🛠️ Tự động fix giúp bạn? (git rm --cached + thêm .gitignore, giữ file ở máy)`,
213
+ default: true
214
+ });
215
+ }
181
216
 
182
- // Bước 4: Commit với thời gian giả lập (Backdate Commit)
183
- spinner.start(`Đang tạo Commit với thời gian [${parsedDate.display}]...`);
184
-
217
+ if (wantFix) {
218
+ spinner.start('Đang tự fix: gỡ file secret khỏi stage…');
219
+ const res = autoFixStagedSecrets(allFiles);
220
+ spinner.succeed(chalk.hex('#34D399')(`✓ Đã gỡ ${res.fixed.length} file khỏi commit`));
221
+ printFixResult(res);
222
+
223
+ // Đảm bảo .gitignore được stage trước khi kiểm tra hasStagedChanges
224
+ try {
225
+ const { execSync } = await import('child_process');
226
+ execSync('git add .gitignore', { stdio: 'ignore' });
227
+ } catch { /* bỏ qua */ }
228
+
229
+ if (!hasStagedChanges()) {
230
+ printErrorBox('KHÔNG CÒN FILE ĐỂ COMMIT', [
231
+ 'Sau khi gỡ secret thì không còn file nào để commit.',
232
+ 'File secret vẫn nằm ở máy bạn, chỉ là không push lên nữa.',
233
+ ]);
234
+ process.exit(0);
235
+ }
236
+ console.log(chalk.hex('#34D399')(' → Tiếp tục commit/push phần sạch còn lại.\n'));
237
+ } else if (isInteractive) {
238
+ const keepGoing = await confirm({
239
+ message: findings.length > 0
240
+ ? `⚠️ Phát hiện ${findings.length} vị trí giống secret. Vẫn tiếp tục mà không fix?`
241
+ : 'Vẫn tiếp tục commit/push với các file trên mà không fix?',
242
+ default: false
243
+ });
244
+ if (!keepGoing) {
245
+ console.log(chalk.cyan('\n 🛠️ Chạy lại với --fix để tool tự gỡ giúp bạn, hoặc làm tay:'));
246
+ allFiles.forEach((f) => console.log(chalk.white(` git rm --cached "${f}"`)));
247
+ console.log(chalk.gray(' Sau đó chạy lại tool.\n'));
248
+ process.exit(0);
249
+ }
250
+ console.log(chalk.yellow(' → Bạn chọn tiếp tục với file nhạy cảm. Nếu push bị chặn, tool sẽ hướng dẫn tiếp.\n'));
251
+ } else {
252
+ console.log(chalk.yellow(' ⚠️ Chế độ -y: vẫn tiếp tục dù phát hiện file nhạy cảm. Dùng --fix để tự gỡ.\n'));
253
+ }
254
+ }
255
+
256
+ // [4/6] Commit
257
+ spinner.start(stepLabel(4, TOTAL_STEPS, `Tạo commit [${parsedDate.display}]…`));
185
258
  if (!hasChangesToCommit()) {
186
- spinner.info('Không có thay đổi nào mới để commit. Sẽ tiến hành đẩy commit hiện có.');
259
+ spinner.info(stepLabel(4, TOTAL_STEPS, chalk.hex('#94A3B8')('Không có thay đổi mới — dùng commit hiện có')));
187
260
  } else {
188
261
  commitWithCustomDate(commitMsg, parsedDate.formatted);
189
- spinner.succeed(`Đã tạo commit thành công với thời gian: ${chalk.bold(parsedDate.display)}`);
262
+ spinner.succeed(stepLabel(4, TOTAL_STEPS, chalk.hex('#34D399')(`Commit hoàn tất • ${chalk.hex('#C084FC').bold(parsedDate.display)}`)));
190
263
  }
191
264
 
192
- // Bước 5: Đặt tên branch
193
- spinner.start(`Đang thiết lập branch chính '${branchName}'...`);
265
+ // [5/6] Branch
266
+ spinner.start(stepLabel(5, TOTAL_STEPS, `Đổi branch → '${branchName}'…`));
194
267
  setBranchName(branchName);
195
- spinner.succeed(`Đã đặt tên branch: ${branchName}`);
268
+ spinner.succeed(stepLabel(5, TOTAL_STEPS, chalk.hex('#34D399')(`Branch: ${chalk.hex('#5EEAD4').bold(branchName)}`)));
196
269
 
197
- // Bước 6: Push lên Remote
198
- spinner.start(`Đang đẩy code lên remote '${repoUrl}' (branch: ${branchName})...`);
270
+ // [6/6] Push
271
+ spinner.start(stepLabel(6, TOTAL_STEPS, `Push lên origin/${branchName}…`));
199
272
  pushToRemote(branchName, options.force);
200
- spinner.succeed(chalk.bold.green(`🎉 ĐÃ ĐẨY DỰ ÁN LÊN GIT THÀNH CÔNG!`));
273
+ spinner.succeed(stepLabel(6, TOTAL_STEPS, chalk.bold.hex('#34D399')('Push lên remote thành công!')));
201
274
 
202
- console.log('\n' + chalk.bold.green('✨ Hoàn tất! Bạn có thể kiểm tra commit log trên GitHub/GitLab của mình.'));
275
+ printSuccessBox({ branch: branchName, repoUrl });
276
+
277
+ // [+1] Publish npm (optional)
278
+ let wantPublish = options.publish === true;
279
+ if (!wantPublish && isInteractive) {
280
+ wantPublish = await confirm({
281
+ message: '📦 Publish package này lên npm luôn không?',
282
+ default: false
283
+ });
284
+ }
285
+ if (wantPublish) {
286
+ await runNpmPublish({ access: options.access, dryRun: options.dryRun, isInteractive });
287
+ } else {
288
+ printFooter();
289
+ }
203
290
  } catch (error) {
204
- spinner.fail(chalk.red('Đã xảy ra lỗi trong quá trình thực thi:'));
205
- console.error(chalk.red(`\nLỗi: ${error.message}\n`));
291
+ spinner.stop();
292
+ if (error.code === 'SECRET_BLOCKED') {
293
+ printSecretBlockedBox(error.message, error.details || {});
294
+
295
+ // Kiểm tra xem có commit unpushed chứa vết secret cũ không để hỗ trợ squash tự động
296
+ const unpushed = getUnpushedCommits(branchName);
297
+ if (unpushed.length > 0) {
298
+ console.log(chalk.cyanBright(`\n 💡 Phát hiện bạn có ${unpushed.length} commit chưa push ở local có thể chứa vết secret cũ.`));
299
+ let doSquash = options.fix === true;
300
+ if (!doSquash && isInteractive) {
301
+ doSquash = await confirm({
302
+ message: '🛠️ Tự động làm sạch lịch sử (squash các commit unpushed thành 1 commit sạch) để push lại?',
303
+ default: true
304
+ });
305
+ }
306
+ if (doSquash) {
307
+ spinner.start('Đang squash và làm sạch commit chưa push…');
308
+ const sqRes = squashUnpushedCommits(branchName, commitMsg, parsedDate.formatted);
309
+ if (sqRes.ok) {
310
+ spinner.succeed(chalk.green('✓ Đã làm sạch commit! Đang thử push lại…'));
311
+ try {
312
+ spinner.start(stepLabel(6, TOTAL_STEPS, `Push lại lên origin/${branchName}…`));
313
+ pushToRemote(branchName, options.force);
314
+ spinner.succeed(chalk.bold.green('✓ Push thành công'));
315
+ printSuccessBox({ branch: branchName, repoUrl });
316
+ printFooter();
317
+ return;
318
+ } catch (retryErr) {
319
+ spinner.fail(chalk.red(`Push lại thất bại: ${retryErr.message}`));
320
+ }
321
+ } else {
322
+ spinner.fail(chalk.red(`Không thể tự động squash: ${sqRes.error}`));
323
+ }
324
+ }
325
+ }
326
+ } else if (error.code === 'AUTH_FAILED') {
327
+ printAuthFailedBox(error.message);
328
+ } else if (error.code === 'NON_FAST_FORWARD') {
329
+ printNonFastForwardBox(error.message, branchName);
330
+ } else if (error.code === 'REMOTE_OR_NETWORK') {
331
+ 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.');
332
+ } else {
333
+ printErrorBox('ĐẨY LÊN GIT THẤT BẠI', [error.message]);
334
+ }
335
+ process.exit(1);
336
+ }
337
+ }
338
+
339
+ /**
340
+ * Lệnh fix độc lập: `git-push-time fix`
341
+ * Tự động quét và fix secret ở staged, working directory và unpushed commits.
342
+ */
343
+ async function runFixCommand() {
344
+ printBanner(VERSION);
345
+ printSection('🛠️ TỰ ĐỘNG FIX SECRET & BẢO VỆ REPO');
346
+
347
+ if (!checkGitInstalled()) {
348
+ printGitMissing();
349
+ process.exit(1);
350
+ }
351
+
352
+ if (!isGitRepo()) {
353
+ printErrorBox('CHƯA PHẢI GIT REPOSITORY', ['Thư mục này chưa phải là Git repository.'], 'Chạy git init hoặc cd vào đúng thư mục dự án.');
206
354
  process.exit(1);
207
355
  }
356
+
357
+ const spinner = ora({ color: 'cyan' });
358
+ spinner.start('Đang kiểm tra staged files và thư mục làm việc…');
359
+
360
+ const risky = findRiskyStagedFiles();
361
+ const findings = scanStagedContentForSecrets();
362
+ const riskyWorking = findRiskyWorkingTreeFiles();
363
+ spinner.stop();
364
+
365
+ const allDetected = [...new Set([...risky, ...findings.map((f) => f.file), ...riskyWorking])];
366
+
367
+ if (allDetected.length === 0) {
368
+ printSection('✨ KẾT QUẢ QUÉT');
369
+ console.log(chalk.hex('#34D399')(' ✨ Không phát hiện file secret mới ở stage hay thư mục làm việc!\n'));
370
+ } else {
371
+ printPrePushSecretWarning({ riskyFiles: allDetected, findings });
372
+ spinner.start('Đang tự động fix (gỡ khỏi stage + thêm .gitignore)…');
373
+ const res = autoFixStagedSecrets(allDetected);
374
+ spinner.succeed(chalk.hex('#34D399')(`✓ Đã xử lý xong ${res.fixed.length} file!`));
375
+ printFixResult(res);
376
+ }
377
+
378
+ // Kiểm tra xem có commit chưa push nào bị dính vết secret cũ không
379
+ const currentBranch = getCurrentBranch();
380
+ const unpushed = getUnpushedCommits(currentBranch);
381
+ if (unpushed.length > 0) {
382
+ printSection(`🔍 ${unpushed.length} COMMIT CHƯA PUSH`);
383
+ unpushed.slice(0, 5).forEach((c) => console.log(chalk.hex('#94A3B8')(` • ${c}`)));
384
+ if (unpushed.length > 5) {
385
+ console.log(chalk.hex('#64748B')(` …và ${unpushed.length - 5} commit khác`));
386
+ }
387
+ const okSquash = await confirm({
388
+ message: `🛠️ Bạn có muốn gộp (squash) ${unpushed.length} commit chưa push thành 1 commit sạch để xóa dấu vết secret cũ không?`,
389
+ default: true
390
+ });
391
+ if (okSquash) {
392
+ spinner.start('Đang làm sạch lịch sử commit chưa push…');
393
+ const sq = squashUnpushedCommits(currentBranch, 'Clean commit without secrets');
394
+ if (sq.ok) {
395
+ spinner.succeed(chalk.hex('#34D399')('✓ Đã làm sạch commit chưa push! Sẵn sàng push lên remote.'));
396
+ } else {
397
+ spinner.fail(chalk.red(`Không thể squash: ${sq.error}`));
398
+ }
399
+ }
400
+ }
401
+
402
+ printSuccessBox({ branch: currentBranch || 'main', repoUrl: 'local repo đã sạch — sẵn sàng push' });
208
403
  }
209
404
 
210
- run();
405
+ /**
406
+ * Publish lên npm sau khi push Git xong.
407
+ * An toàn: không nhận token qua flag, dùng `npm login` hoặc env NPM_TOKEN.
408
+ * Nếu .npmrc đang bị track trong git thì cảnh báo (không in token).
409
+ */
410
+ async function runNpmPublish({ access = 'public', dryRun = false, isInteractive = true } = {}) {
411
+ const spinner = ora({ color: 'magenta' });
412
+ printSection('📦 PUBLISH LÊN NPM');
413
+
414
+ if (!checkNpmInstalled()) {
415
+ printErrorBox('CHƯA CÀI NPM', ['Chưa phát hiện npm trên máy.'], 'Cài Node.js từ https://nodejs.org rồi thử lại.');
416
+ return;
417
+ }
418
+ const pkg = getPackageInfo();
419
+ if (!pkg) {
420
+ printErrorBox('KHÔNG ĐỌC ĐƯỢC PACKAGE.JSON', ['Không tìm thấy package.json ở thư mục hiện tại.'], 'cd vào đúng thư mục dự án rồi chạy lại.');
421
+ return;
422
+ }
423
+ console.log(chalk.gray(` 📦 Package: ${chalk.white(pkg.name)}@${chalk.white(pkg.version)} • access: ${access}${dryRun ? ' • dry-run' : ''}`));
424
+
425
+ if (isNpmrcTracked()) {
426
+ console.log(chalk.yellowBright('\n ⚠️ Phát hiện .npmrc đang nằm trong git.'));
427
+ console.log(chalk.gray(' File này thường chứa token, không nên commit/publish cùng code.'));
428
+ console.log(chalk.gray(' Tool sẽ publish mà không cần commit .npmrc — hãy gỡ nó: git rm --cached .npmrc\n'));
429
+ }
430
+
431
+ const login = checkNpmLogin();
432
+ if (!login.loggedIn && !process.env.NPM_TOKEN && !process.env.NODE_AUTH_TOKEN) {
433
+ console.log(chalk.yellow(' 🔒 Chưa login npm.'));
434
+ console.log(chalk.cyan(' 👉 Chạy: npm login (hoặc export NPM_TOKEN=... khi chạy CI)'));
435
+ if (isInteractive) {
436
+ const go = await confirm({ message: 'Đã login xong, publish tiếp?', default: false });
437
+ if (!go) {
438
+ console.log(chalk.gray(' Đã bỏ qua publish npm.\n'));
439
+ return;
440
+ }
441
+ } else {
442
+ console.log(chalk.gray(' Chế độ -y mà chưa login nên bỏ qua publish.\n'));
443
+ return;
444
+ }
445
+ } else if (login.loggedIn) {
446
+ console.log(chalk.gray(` 👤 npm user: ${login.user}`));
447
+ }
448
+
449
+ spinner.start(dryRun ? 'Chạy thử npm publish --dry-run…' : `Đang publish ${pkg.name}@${pkg.version}…`);
450
+ const res = publishToNpm({ access, dryRun, cwd: process.cwd() });
451
+ if (res.ok) {
452
+ spinner.stop();
453
+ printNpmResult({ ok: true, pkgName: pkg.name, version: pkg.version, dryRun });
454
+ printFooter();
455
+ return;
456
+ }
457
+
458
+ spinner.fail(chalk.red('Publish thất bại'));
459
+ printNpmResult({ ok: false, pkgName: pkg.name, version: pkg.version, output: res.output, hint: res.parsed.hint, dryRun });
460
+ if (res.parsed.code === 'VERSION_EXISTS' && isInteractive) {
461
+ const bump = await confirm({ message: 'Tự tăng patch version (npm version patch) rồi publish lại?', default: true });
462
+ if (bump) {
463
+ const { execSync } = await import('child_process');
464
+ try {
465
+ execSync('npm version patch --no-git-tag-version', { stdio: 'ignore' });
466
+ console.log(chalk.hex('#34D399')(' ✓ Đã tăng version, chạy lại tool với --publish để publish bản mới.\n'));
467
+ } catch {
468
+ printErrorBox('KHÔNG TĂNG ĐƯỢC VERSION', ['Kiểm tra lại package.json.']);
469
+ }
470
+ }
471
+ }
472
+ printFooter();
473
+ }
474
+
475
+ const program = new Command();
476
+ styleCliHelp(program);
477
+
478
+ program
479
+ .name('git-push-time')
480
+ .description('Đẩy dự án lên Git siêu tốc + tùy chỉnh thời gian commit (Backdate Commit)')
481
+ .version(VERSION)
482
+ .option('-r, --repo <url>', 'Đường dẫn Git repository (URL)')
483
+ .option('-d, --date <datetime>', 'Thời gian commit (Ví dụ: "2024-01-15 14:30:00", "-2d", "-5h", "now")')
484
+ .option('-m, --message <msg>', 'Thông điệp commit (Commit message)')
485
+ .option('-b, --branch <branch>', 'Tên branch trên remote (Mặc định: main)')
486
+ .option('-f, --force', 'Thực hiện push đè (--force)', false)
487
+ .option('-y, --yes', 'Bỏ qua prompt tương tác, tự động dùng tham số hoặc giá trị mặc định', false)
488
+ .option('--fix', 'Tự động gỡ file secret khỏi stage + thêm .gitignore (an toàn, không xóa file thật)', false)
489
+ .option('--publish', 'Publish lên npm sau khi push Git thành công', false)
490
+ .option('--access <mode>', 'Quyền publish npm (public|restricted)', 'public')
491
+ .option('--dry-run', 'Chạy thử npm publish mà không publish thật', false)
492
+ .action(async (opts) => {
493
+ await run(opts);
494
+ });
495
+
496
+ program
497
+ .command('fix')
498
+ .description('Tự động quét và gỡ file secret khỏi stage, thêm vào .gitignore và làm sạch commit chưa push')
499
+ .action(async () => {
500
+ await runFixCommand();
501
+ });
502
+
503
+ program.parse(process.argv);