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/README.md +151 -47
- package/bin/index.js +231 -443
- package/lib/commands/edit-time.js +127 -0
- package/lib/commands/fix.js +105 -0
- package/lib/commands/github.js +436 -0
- package/lib/commands/pull.js +88 -0
- package/lib/commands/push.js +653 -0
- package/lib/config.js +210 -0
- package/lib/git.js +222 -13
- package/lib/github.js +304 -0
- package/lib/prompt-helpers.js +62 -0
- package/lib/ui.js +683 -248
- package/package.json +4 -4
- package/lib/npm.js +0 -82
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
|
|
5
|
-
import
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
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
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
-
|
|
22
|
+
printDashboardStatus,
|
|
39
23
|
printSection,
|
|
40
|
-
stepLabel,
|
|
41
|
-
printPrePushSecretWarning,
|
|
42
|
-
printSecretBlockedBox,
|
|
43
|
-
printSuccessBox,
|
|
44
|
-
printCancelled,
|
|
45
|
-
printErrorBox,
|
|
46
24
|
printGitMissing,
|
|
47
|
-
|
|
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
|
|
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
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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
|
-
|
|
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
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
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
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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
|
-
|
|
257
|
-
|
|
258
|
-
|
|
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
|
-
|
|
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
|
-
|
|
450
|
-
|
|
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
|
-
|
|
459
|
-
|
|
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('
|
|
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 (
|
|
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
|
|
488
|
-
.option('--fix', 'Tự động gỡ file secret khỏi stage
|
|
489
|
-
.
|
|
490
|
-
|
|
491
|
-
|
|
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
|
-
|
|
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('
|
|
498
|
-
.description('
|
|
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
|
-
|
|
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);
|