git-push-time 1.0.2 → 1.1.0

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/bin/index.js CHANGED
@@ -1,503 +1,291 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  import { Command } from 'commander';
4
- import chalk from 'chalk';
5
- import ora from 'ora';
6
- import { input, select, confirm } from '@inquirer/prompts';
7
- import { isValidGitUrl, parseCustomDate } from '../lib/utils.js';
4
+ import { select } from '@inquirer/prompts';
5
+ import { checkGitInstalled, getCurrentBranch } from '../lib/git.js';
6
+ import { getCurrentGitHubUser } from '../lib/github.js';
7
+ import { runPushWizard } from '../lib/commands/push.js';
8
+ import { runPullCommand } from '../lib/commands/pull.js';
9
+ import { runEditCommitTimeCommand } from '../lib/commands/edit-time.js';
10
+ import { runFixCommand } from '../lib/commands/fix.js';
8
11
  import {
9
- checkGitInstalled,
10
- isGitRepo,
11
- initGitRepo,
12
- setGitRemote,
13
- stageAllFiles,
14
- hasChangesToCommit,
15
- commitWithCustomDate,
16
- setBranchName,
17
- pushToRemote,
18
- getCurrentBranch,
19
- findRiskyStagedFiles
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';
12
+ runGitHubMenu,
13
+ showGitHubStatus,
14
+ promptCreateRepo,
15
+ showUserRepos,
16
+ promptGitHubLogin,
17
+ logoutGitHub
18
+ } from '../lib/commands/github.js';
19
+ import { promptContinueOrBack } from '../lib/prompt-helpers.js';
36
20
  import {
37
21
  printBanner,
38
- printSummary,
22
+ printDashboardStatus,
39
23
  printSection,
40
- stepLabel,
41
- printPrePushSecretWarning,
42
- printSecretBlockedBox,
43
- printSuccessBox,
44
- printCancelled,
45
- printErrorBox,
46
24
  printGitMissing,
47
- printAuthFailedBox,
48
- printNonFastForwardBox,
49
- printFixResult,
50
- printNpmResult,
51
- printFooter,
25
+ printCancelled,
52
26
  styleCliHelp
53
27
  } from '../lib/ui.js';
54
28
 
55
- const VERSION = '1.0.2';
56
- const TOTAL_STEPS = 6;
57
-
58
- async function run(options = {}) {
59
- printBanner(VERSION);
60
-
61
- // 1. Kiểm tra Git
62
- if (!checkGitInstalled()) {
63
- printBanner(VERSION);
64
- printGitMissing();
65
- process.exit(1);
66
- }
67
-
68
- let repoUrl = options.repo;
69
- let commitMsg = options.message;
70
- let dateInput = options.date;
71
- let branchName = options.branch || 'main';
72
-
73
- const isInteractive = !options.yes;
74
-
75
- // 2. Thu thập thông tin
76
- if (isInteractive) {
77
- printSection('🔗 KẾT NỐI REPOSITORY');
78
-
79
- if (!repoUrl) {
80
- repoUrl = await input({
81
- message: '📦 Dán link Git Repository:',
82
- validate: (value) => {
83
- if (!value || !value.trim()) return 'Vui lòng nhập đường dẫn repository!';
84
- if (!isValidGitUrl(value)) return 'URL chưa đúng (vd: https://github.com/user/repo.git)';
85
- return true;
86
- }
87
- });
88
- }
89
-
90
- if (!commitMsg) {
91
- commitMsg = await input({
92
- message: '💬 Thông điệp commit:',
93
- default: 'Initial commit'
94
- });
95
- }
29
+ const VERSION = '1.1.0';
96
30
 
97
- if (!dateInput) {
98
- printSection('⏰ THỜI GIAN COMMIT');
99
- const dateOption = await select({
100
- message: 'Chọn thời gian cho commit:',
101
- choices: [
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' }
105
- ]
106
- });
31
+ /**
32
+ * Kiểm tra xem lệnh có được gọi "trần" (không flag/subcommand nào) hay không.
33
+ */
34
+ function isBareInvocation(options = {}) {
35
+ return (
36
+ !options.repo &&
37
+ !options.message &&
38
+ !options.date &&
39
+ !options.branch &&
40
+ !options.dir &&
41
+ !options.force &&
42
+ !options.fix
43
+ );
44
+ }
107
45
 
108
- if (dateOption === 'now') {
109
- dateInput = 'now';
110
- } else if (dateOption === 'relative') {
111
- dateInput = await input({
112
- message: '⏳ Nhập thời gian lùi (VD: -2d, -5h, -30m, -1w):',
113
- default: '-1d',
114
- validate: (val) => {
115
- const parsed = parseCustomDate(val);
116
- return parsed.valid || parsed.error || 'Thời gian không hợp lệ';
117
- }
118
- });
119
- } else if (dateOption === 'custom') {
120
- dateInput = await input({
121
- message: '📅 Nhập ngày giờ (YYYY-MM-DD HH:mm:ss):',
122
- default: '2024-01-15 10:00:00',
123
- validate: (val) => {
124
- const parsed = parseCustomDate(val);
125
- return parsed.valid || parsed.error || 'Ngày giờ không hợp lệ';
126
- }
127
- });
46
+ /**
47
+ * Menu chính khi chạy `git-push-time` không truyền flag
48
+ */
49
+ async function promptMainMenu() {
50
+ printSection('BẢNG ĐIỀU KHIỂN CHÍNH');
51
+ return await select({
52
+ message: 'Bạn muốn thực hiện thao tác nào?',
53
+ pageSize: 10,
54
+ choices: [
55
+ {
56
+ name: '» [PUSH] | Đẩy code lên Git (kèm chọn folder, backdate & chống lộ secret)',
57
+ value: 'push',
58
+ description: 'Đẩy dự án lên remote repository hiện tại hoặc mới'
59
+ },
60
+ {
61
+ name: '» [GITHUB] | Quản lý GitHub & Tạo repository mới',
62
+ value: 'github',
63
+ description: 'Tạo repo mới trên GitHub, xem profile, chọn repo nhanh'
64
+ },
65
+ {
66
+ name: '» [PULL] | Cập nhật code mới về máy',
67
+ value: 'pull',
68
+ description: 'git pull --ff-only hoặc --rebase từ remote'
69
+ },
70
+ {
71
+ name: '» [TIME] | Sửa thời gian commit lịch sử',
72
+ value: 'edit-time',
73
+ description: 'Đổi Author/Committer Date của commit trong quá khứ'
74
+ },
75
+ {
76
+ name: '» [FIX] | Quét & gỡ sạch secret khỏi repo',
77
+ value: 'fix',
78
+ description: 'Quét file nhạy cảm, gỡ khỏi stage và squash commit'
79
+ },
80
+ {
81
+ name: 'x [THOÁT] | Kết thúc chương trình',
82
+ value: 'exit',
83
+ description: 'Thoát ứng dụng'
128
84
  }
129
- }
130
-
131
- if (!options.branch) {
132
- const currentB = getCurrentBranch();
133
- branchName = await input({
134
- message: '🌿 Branch trên remote:',
135
- default: currentB || 'main'
136
- });
137
- }
138
- }
139
-
140
- if (!commitMsg) commitMsg = 'Initial commit';
141
- if (!dateInput) dateInput = 'now';
142
- if (!branchName) branchName = 'main';
143
-
144
- if (!repoUrl || !isValidGitUrl(repoUrl)) {
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');
148
- process.exit(1);
149
- }
150
-
151
- const parsedDate = parseCustomDate(dateInput);
152
- if (!parsedDate.valid) {
153
- printErrorBox('THỜI GIAN KHÔNG HỢP LỆ', [parsedDate.error], 'VD: now, -2d, -5h, 2024-01-15 14:30:00');
154
- process.exit(1);
155
- }
156
-
157
- // Tóm tắt
158
- printSummary({
159
- repoUrl,
160
- commitMsg,
161
- dateDisplay: parsedDate.display,
162
- branch: branchName,
163
- force: options.force
85
+ ]
164
86
  });
87
+ }
165
88
 
166
- if (isInteractive) {
167
- const ok = await confirm({
168
- message: '🚀 Xác nhận đẩy lên Git ngay?',
169
- default: true
89
+ /**
90
+ * Vòng lặp Menu chính
91
+ */
92
+ async function runMainMenuLoop(initialOptions = {}) {
93
+ while (true) {
94
+ const cwd = process.cwd();
95
+ const branch = getCurrentBranch(cwd);
96
+ const ghStatus = await getCurrentGitHubUser();
97
+
98
+ printDashboardStatus({
99
+ targetDir: cwd,
100
+ currentBranch: branch,
101
+ githubUser: ghStatus.authenticated ? ghStatus.user : null
170
102
  });
171
- if (!ok) {
103
+
104
+ const action = await promptMainMenu();
105
+
106
+ if (action === 'exit') {
172
107
  printCancelled();
173
108
  process.exit(0);
174
109
  }
175
- }
176
110
 
177
- // 3. Thực thi
178
- const spinner = ora({ color: 'cyan' });
179
- printSection(`⚙️ ĐANG TRIỂN KHAI (${TOTAL_STEPS} bước)`);
180
-
181
- try {
182
- // [1/6] Git init
183
- if (!isGitRepo()) {
184
- spinner.start(stepLabel(1, TOTAL_STEPS, 'Khởi tạo Git repo (git init)…'));
185
- initGitRepo();
186
- spinner.succeed(stepLabel(1, TOTAL_STEPS, chalk.hex('#34D399')('Đã khởi tạo Git repo')));
187
- } else {
188
- console.log(' ' + stepLabel(1, TOTAL_STEPS, chalk.hex('#64748B')('Đã có Git repo sẵn — bỏ qua init')));
111
+ if (action === 'push') {
112
+ await runPushWizard(initialOptions, async () => {
113
+ // Callback quay lại menu chính
114
+ return await runMainMenuLoop(initialOptions);
115
+ });
116
+ return;
189
117
  }
190
118
 
191
- // [2/6] Remote
192
- spinner.start(stepLabel(2, TOTAL_STEPS, 'Cấu hình remote origin…'));
193
- setGitRemote(repoUrl);
194
- spinner.succeed(stepLabel(2, TOTAL_STEPS, chalk.hex('#34D399')('Cấu hình remote origin hoàn tất')));
195
-
196
- // [3/6] Stage
197
- spinner.start(stepLabel(3, TOTAL_STEPS, 'Stage files (git add .)…'));
198
- stageAllFiles();
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
- }
216
-
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'));
119
+ if (action === 'github') {
120
+ const ghResult = await runGitHubMenu({ defaultDir: cwd });
121
+ if (ghResult && ghResult.autoPush && ghResult.repoUrl) {
122
+ // Sau khi tạo repo mới, chuyển tiếp thẳng vào luồng push
123
+ await runPushWizard({ ...initialOptions, repo: ghResult.repoUrl });
124
+ return;
253
125
  }
126
+ } else if (action === 'pull') {
127
+ await runPullCommand('origin', undefined, { fromMenu: true });
128
+ } else if (action === 'edit-time') {
129
+ await runEditCommitTimeCommand({ fromMenu: true });
130
+ } else if (action === 'fix') {
131
+ await runFixCommand({ fromMenu: true });
254
132
  }
255
133
 
256
- // [4/6] Commit
257
- spinner.start(stepLabel(4, TOTAL_STEPS, `Tạo commit [${parsedDate.display}]…`));
258
- if (!hasChangesToCommit()) {
259
- spinner.info(stepLabel(4, TOTAL_STEPS, chalk.hex('#94A3B8')('Không có thay đổi mới — dùng commit hiện có')));
260
- } else {
261
- commitWithCustomDate(commitMsg, parsedDate.formatted);
262
- spinner.succeed(stepLabel(4, TOTAL_STEPS, chalk.hex('#34D399')(`Commit hoàn tất • ${chalk.hex('#C084FC').bold(parsedDate.display)}`)));
263
- }
264
-
265
- // [5/6] Branch
266
- spinner.start(stepLabel(5, TOTAL_STEPS, `Đổi branch → '${branchName}'…`));
267
- setBranchName(branchName);
268
- spinner.succeed(stepLabel(5, TOTAL_STEPS, chalk.hex('#34D399')(`Branch: ${chalk.hex('#5EEAD4').bold(branchName)}`)));
269
-
270
- // [6/6] Push
271
- spinner.start(stepLabel(6, TOTAL_STEPS, `Push lên origin/${branchName}…`));
272
- pushToRemote(branchName, options.force);
273
- spinner.succeed(stepLabel(6, TOTAL_STEPS, chalk.bold.hex('#34D399')('Push lên remote thành công!')));
274
-
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
- }
290
- } catch (error) {
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]);
134
+ const next = await promptContinueOrBack('Hoàn tất! Bạn muốn tiếp tục hay quay lại bảng điều khiển?');
135
+ if (next === 'exit') {
136
+ process.exit(0);
334
137
  }
335
- process.exit(1);
336
138
  }
337
139
  }
338
140
 
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() {
141
+ async function main(options = {}) {
344
142
  printBanner(VERSION);
345
- printSection('🛠️ TỰ ĐỘNG FIX SECRET & BẢO VỆ REPO');
346
143
 
347
144
  if (!checkGitInstalled()) {
348
145
  printGitMissing();
349
146
  process.exit(1);
350
147
  }
351
148
 
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.');
354
- process.exit(1);
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' });
403
- }
404
-
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
- }
149
+ const isInteractive = !options.yes;
448
150
 
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();
151
+ if (isInteractive && isBareInvocation(options)) {
152
+ await runMainMenuLoop(options);
455
153
  return;
456
154
  }
457
155
 
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();
156
+ // Chạy trực tiếp push wizard với các options được cung cấp
157
+ await runPushWizard(options);
473
158
  }
474
159
 
160
+ /* ==========================================================================
161
+ COMMANDER CLI CONFIGURATION
162
+ ========================================================================== */
163
+
475
164
  const program = new Command();
476
165
  styleCliHelp(program);
477
166
 
478
167
  program
479
168
  .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)')
169
+ .description('Bộ công cụ Git & GitHub CLI: Đẩy code siêu tốc, kết nối GitHub tạo repo, chỉnh thời gian commit và bảo vệ secret')
481
170
  .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")')
171
+ .option('-r, --repo <url>', 'Đường dẫn Git repository (URL hoặc để trống để chọn/tạo mới)')
172
+ .option('-d, --date <datetime>', 'Thời gian commit (VD: "2024-01-15 14:30:00", "-2d", "-5h", "now")')
484
173
  .option('-m, --message <msg>', 'Thông điệp commit (Commit message)')
485
174
  .option('-b, --branch <branch>', 'Tên branch trên remote (Mặc định: main)')
175
+ .option('-C, --dir <path>', 'Thư mục nguồn cần push (Mặc định: thư mục hiện tại)')
486
176
  .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)
177
+ .option('-y, --yes', 'Bỏ qua giao diện hỏi, tự động dùng tham số hoặc giá trị mặc định', false)
178
+ .option('--fix', 'Tự động gỡ file secret khỏi stage & thêm .gitignore (an toàn, không xóa file thật)', false)
179
+ .action(async (opts) => {
180
+ await main(opts);
181
+ });
182
+
183
+ // LỆNH GITHUB
184
+ const ghCmd = program
185
+ .command('github')
186
+ .description('Quản lý tài khoản GitHub và tạo repository mới trực tiếp')
187
+ .action(async () => {
188
+ printBanner(VERSION);
189
+ await runGitHubMenu();
190
+ });
191
+
192
+ ghCmd
193
+ .command('create')
194
+ .description('Tạo repository mới trên GitHub (Public hoặc Private)')
195
+ .option('-C, --dir <path>', 'Thư mục nguồn cần liên kết')
492
196
  .action(async (opts) => {
493
- await run(opts);
197
+ printBanner(VERSION);
198
+ const res = await promptCreateRepo({ targetDir: opts.dir || process.cwd() });
199
+ if (res.ok && res.repo) {
200
+ console.log(`\n » Repo URL: ${res.repo.cloneUrl}\n`);
201
+ }
202
+ });
203
+
204
+ ghCmd
205
+ .command('whoami')
206
+ .description('Xem thông tin tài khoản GitHub đang kết nối')
207
+ .action(async () => {
208
+ printBanner(VERSION);
209
+ await showGitHubStatus();
210
+ });
211
+
212
+ ghCmd
213
+ .command('repos')
214
+ .description('Liệt kê danh sách repository trên GitHub của bạn')
215
+ .action(async () => {
216
+ printBanner(VERSION);
217
+ await showUserRepos();
218
+ });
219
+
220
+ ghCmd
221
+ .command('login')
222
+ .description('Đăng nhập tài khoản GitHub bằng OAuth Device Flow hoặc Token')
223
+ .action(async () => {
224
+ printBanner(VERSION);
225
+ await promptGitHubLogin();
226
+ });
227
+
228
+ ghCmd
229
+ .command('logout')
230
+ .description('Đăng xuất tài khoản GitHub khỏi máy')
231
+ .action(async () => {
232
+ printBanner(VERSION);
233
+ logoutGitHub();
494
234
  });
495
235
 
236
+ // LỆNH LOGIN / LOGOUT / WHOAMI TRỰC TIẾP
496
237
  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')
238
+ .command('login')
239
+ .description('Đăng nhập GitHub qua OAuth Device Flow (mở trình duyệt) hoặc Token')
499
240
  .action(async () => {
500
- await runFixCommand();
241
+ printBanner(VERSION);
242
+ await promptGitHubLogin();
243
+ });
244
+
245
+ program
246
+ .command('whoami')
247
+ .description('Xem thông tin tài khoản GitHub đang kết nối')
248
+ .action(async () => {
249
+ printBanner(VERSION);
250
+ await showGitHubStatus();
251
+ });
252
+
253
+ program
254
+ .command('logout')
255
+ .description('Đăng xuất tài khoản GitHub khỏi máy')
256
+ .action(async () => {
257
+ printBanner(VERSION);
258
+ logoutGitHub();
259
+ });
260
+
261
+ // LỆNH FIX
262
+ program
263
+ .command('fix')
264
+ .description('Quét & tự động gỡ file secret khỏi stage, cập nhật .gitignore và làm sạch commit unpushed')
265
+ .option('-C, --dir <path>', 'Thư mục cần quét fix (Mặc định: thư mục hiện tại)')
266
+ .action(async (opts) => {
267
+ printBanner(VERSION);
268
+ await runFixCommand(opts);
269
+ });
270
+
271
+ // LỆNH PULL
272
+ program
273
+ .command('pull [remote] [branch]')
274
+ .description('Cập nhật code từ remote bằng fast-forward --ff-only hoặc --rebase')
275
+ .option('-C, --dir <path>', 'Thư mục repo cần pull (Mặc định: thư mục hiện tại)')
276
+ .action(async (remote, branch, opts) => {
277
+ printBanner(VERSION);
278
+ await runPullCommand(remote || 'origin', branch, opts || {});
279
+ });
280
+
281
+ // LỆNH EDIT-TIME
282
+ program
283
+ .command('edit-time')
284
+ .description('Sửa thời gian của commit lịch sử (Backdate commit hash)')
285
+ .option('-C, --dir <path>', 'Thư mục repo (Mặc định: thư mục hiện tại)')
286
+ .action(async (opts) => {
287
+ printBanner(VERSION);
288
+ await runEditCommitTimeCommand(opts);
501
289
  });
502
290
 
503
291
  program.parse(process.argv);