git-push-time 1.0.1 → 1.0.3

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/lib/ui.js CHANGED
@@ -1,409 +1,728 @@
1
- import chalk from 'chalk';
2
- import stringWidth from 'string-width';
1
+ import chalk from "chalk";
2
+ import stringWidth from "string-width";
3
3
 
4
- const W = 66; // Chiều rộng chuẩn của card
4
+ /**
5
+ * Tự động tính toán chiều rộng card linh hoạt theo Terminal (64 - 78 ký tự)
6
+ */
7
+ function getW() {
8
+ const cols = process.stdout.columns || 70;
9
+ return Math.min(78, Math.max(64, cols - 4));
10
+ }
5
11
 
6
- // Chuyển mã hex thành [r, g, b]
7
12
  function hexToRgb(hex) {
8
- const n = parseInt(hex.replace('#', ''), 16);
13
+ const n = parseInt(hex.replace("#", ""), 16);
9
14
  return [(n >> 16) & 255, (n >> 8) & 255, n & 255];
10
15
  }
11
16
 
12
17
  /**
13
- * Tạo gradient màu mượt mà qua một mảng các màu hex
18
+ * Tạo gradient màu mượt mà theo chuẩn RGB Interpolation
14
19
  */
15
- export function multiGradient(text, colors = ['#8B5CF6', '#3B82F6', '#06B6D4']) {
20
+ export function multiGradient(
21
+ text,
22
+ colors = ["#C084FC", "#6366F1", "#38BDF8", "#34D399"],
23
+ ) {
16
24
  const chars = [...text];
17
25
  const len = chars.length || 1;
18
26
  const rgbColors = colors.map(hexToRgb);
19
27
 
20
- return chars.map((c, i) => {
21
- const t = (i / (len - 1 || 1)) * (rgbColors.length - 1);
22
- const idx = Math.min(Math.floor(t), rgbColors.length - 2);
23
- const localT = t - idx;
24
- const [r1, g1, b1] = rgbColors[idx];
25
- const [r2, g2, b2] = rgbColors[idx + 1];
26
- const r = Math.round(r1 + (r2 - r1) * localT);
27
- const g = Math.round(g1 + (g2 - g1) * localT);
28
- const b = Math.round(b1 + (b2 - b1) * localT);
29
- return chalk.rgb(r, g, b)(c);
30
- }).join('');
28
+ return chars
29
+ .map((c, i) => {
30
+ const t = (i / (len - 1 || 1)) * (rgbColors.length - 1);
31
+ const idx = Math.min(Math.floor(t), rgbColors.length - 2);
32
+ const localT = t - idx;
33
+ const [r1, g1, b1] = rgbColors[idx];
34
+ const [r2, g2, b2] = rgbColors[idx + 1];
35
+ const r = Math.round(r1 + (r2 - r1) * localT);
36
+ const g = Math.round(g1 + (g2 - g1) * localT);
37
+ const b = Math.round(b1 + (b2 - b1) * localT);
38
+ return chalk.rgb(r, g, b)(c);
39
+ })
40
+ .join("");
31
41
  }
32
42
 
33
- /**
34
- * Căn lề chuẩn xác dựa trên chiều rộng hiển thị thực tế (hỗ trợ Emoji và Unicode)
35
- */
36
43
  function pad(str, len) {
37
44
  const vLen = stringWidth(str);
38
45
  if (vLen >= len) return str;
39
- return str + ' '.repeat(len - vLen);
46
+ return str + " ".repeat(len - vLen);
40
47
  }
41
48
 
42
- /**
43
- * Cắt ngắn chuỗi nếu quá dài theo chiều rộng terminal thực tế
44
- */
45
49
  function truncateVisual(str, maxLen) {
46
50
  if (stringWidth(str) <= maxLen) return str;
47
- let res = '';
51
+ let res = "";
48
52
  for (const ch of str) {
49
- if (stringWidth(res + ch + '…') > maxLen) {
50
- return res + '…';
53
+ if (stringWidth(res + ch + "…") > maxLen) {
54
+ return res + "…";
51
55
  }
52
56
  res += ch;
53
57
  }
54
- return res + '…';
58
+ return res + "…";
55
59
  }
56
60
 
57
- function boxTop(title = '', borderColor = chalk.hex('#3B82F6')) {
58
- if (!title) {
59
- return borderColor('╭' + '─'.repeat(W - 2) + '╮');
60
- }
61
+ /* ==========================================================================
62
+ HEAVY-DUTY BOX ENGINE (Bo góc mềm & Căn lề chuẩn mực)
63
+ ========================================================================== */
64
+
65
+ function boxTop(title = "", borderColor = chalk.hex("#334155")) {
66
+ const W = getW();
67
+ if (!title) return borderColor("╭" + "─".repeat(W - 2) + "╮");
68
+
61
69
  const titleWidth = stringWidth(title);
62
- const leftDash = '─'.repeat(3);
70
+ const leftDash = "─".repeat(3);
63
71
  const rightDashLen = Math.max(0, W - 2 - 3 - titleWidth);
64
- return borderColor('╭' + leftDash) + title + borderColor('─'.repeat(rightDashLen) + '╮');
72
+ return (
73
+ borderColor("╭" + leftDash) +
74
+ title +
75
+ borderColor("─".repeat(rightDashLen) + "╮")
76
+ );
65
77
  }
66
78
 
67
- function boxBottom(borderColor = chalk.hex('#3B82F6')) {
68
- return borderColor('╰' + '─'.repeat(W - 2) + '╯');
79
+ function boxBottom(borderColor = chalk.hex("#334155")) {
80
+ const W = getW();
81
+ return borderColor("╰" + "─".repeat(W - 2) + "╯");
69
82
  }
70
83
 
71
- function boxDivider(borderColor = chalk.hex('#3B82F6')) {
72
- return borderColor('├' + '─'.repeat(W - 2) + '┤');
84
+ function boxRow(content = "", borderColor = chalk.hex("#334155")) {
85
+ const W = getW();
86
+ return borderColor("│ ") + pad(content, W - 4) + borderColor(" │");
73
87
  }
74
88
 
75
- function boxRow(content = '', borderColor = chalk.hex('#3B82F6')) {
76
- return borderColor('│ ') + pad(content, W - 4) + borderColor(' │');
89
+ /**
90
+ * Hàng Key-Value có đường nối bằng dấu chấm ․ ․ ․
91
+ */
92
+ function boxKvDot(
93
+ label,
94
+ value,
95
+ labelWidth = 14,
96
+ borderColor = chalk.hex("#334155"),
97
+ ) {
98
+ const W = getW();
99
+ const maxValWidth = W - 6 - labelWidth;
100
+ const truncatedVal = truncateVisual(value, maxValWidth);
101
+ const valWidth = stringWidth(truncatedVal);
102
+
103
+ const dotsLen = Math.max(1, W - 6 - labelWidth - valWidth);
104
+ const dots = chalk.hex("#1E293B")("․".repeat(dotsLen));
105
+ const formattedLabel = chalk.hex("#94A3B8")(pad(label, labelWidth));
106
+
107
+ return boxRow(`${formattedLabel}${dots}${truncatedVal}`, borderColor);
77
108
  }
78
109
 
110
+ /* ==========================================================================
111
+ PUBLIC INTERFACE COMPONENTS
112
+ ========================================================================== */
113
+
79
114
  /**
80
- * Banner thương hiệu cao cấp dạng ASCII Art + Gradient
81
- */
82
- export function printBanner(version = '1.0.0') {
115
+ * Banner ASCII Neon Glow — ấn tượng bằng gradient + badge pill, không dùng emoji.
116
+ */
117
+ export function printBanner(version = "1.0.0") {
83
118
  const ascii = [
84
- ' ____ _ _ ____ _ _____ _ ',
85
- ' / ___(_) |_ | _ \\ _ _ ___| |__ |_ _(_)_ __ ___ ___ ',
86
- ' | | _| | __| | |_) | | | / __| \'_ \\ | | | | \'_ ` _ \\ / _ \\ ',
87
- ' | |_| | | |_ | __/| |_| \\__ \\ | | | | | | | | | | | | __/ ',
88
- ' \\____|_|\\__| |_| \\__,_|___/_| |_| |_| |_|_| |_| |_|\\___| '
119
+ " ____ _ _ ____ _ _____ _ ",
120
+ " / ___(_) |_ | _ \\ _ _ ___| |__ |_ _(_)_ __ ___ ___ ",
121
+ " | | _| | __| | |_) | | | / __| '_ \\ | | | | '_ ` _ \\ / _ \\ ",
122
+ " | |_| | | |_ | __/| |_| \\__ \\ | | | | | | | | | | | | __/ ",
123
+ " \\____|_|\\__| |_| \\__,_|___/_| |_| |_| |_|_| |_| |_|\\___| ",
89
124
  ];
90
125
 
91
- console.log('');
92
- const colors = ['#A855F7', '#6366F1', '#38BDF8', '#34D399'];
126
+ console.log("");
127
+ const colors = ["#A855F7", "#6366F1", "#38BDF8", "#34D399"];
93
128
  ascii.forEach((l) => console.log(multiGradient(l, colors)));
94
129
 
95
- const badgeVer = chalk.bgHex('#1E293B').hex('#38BDF8').bold(` 🚀 v${version} `);
96
- const badgeSpeed = chalk.bgHex('#1E293B').hex('#A78BFA').bold(' ⚡ Siêu Tốc ');
97
- const badgeShield = chalk.bgHex('#1E293B').hex('#34D399').bold(' 🛡️ Secret Shield ');
98
- const badgeDate = chalk.bgHex('#1E293B').hex('#FBBF24').bold(' ⏰ Backdate Commit ');
130
+ const badgeVer = chalk.bgHex("#1E1B4B").hex("#818CF8").bold(` v${version} `);
131
+ const badgeSpeed = chalk.bgHex("#064E3B").hex("#34D399").bold(" SIÊU TỐC ");
132
+ const badgeShield = chalk
133
+ .bgHex("#0284C7")
134
+ .hex("#E0F2FE")
135
+ .bold(" SECRET SHIELD ");
136
+ const badgeDate = chalk.bgHex("#78350F").hex("#FDE047").bold(" BACKDATE ");
99
137
 
100
138
  console.log(`\n ${badgeVer} ${badgeSpeed} ${badgeShield} ${badgeDate}\n`);
101
139
  }
102
140
 
103
141
  /**
104
- * Tiêu đề phân đoạn với styling thanh lịch
105
- */
142
+ * Tiêu đề phân đoạn — ấn tượng bằng chữ in hoa + gạch nối, không dùng icon.
143
+ */
106
144
  export function printSection(title) {
107
- const icon = chalk.hex('#38BDF8')('✦');
108
- const titleStyled = chalk.bold.hex('#F1F5F9')(` ${title} `);
109
- const prefix = chalk.hex('#475569')('─── ') + icon + titleStyled;
145
+ const W = getW();
146
+ const clean = String(title || "").toUpperCase();
147
+ const marker = chalk.hex("#38BDF8").bold("»");
148
+ const titleStyled = chalk.bold.hex("#F8FAFC")(` ${clean} `);
149
+ const prefix = chalk.hex("#334155")("── ") + marker + titleStyled;
110
150
  const remaining = Math.max(0, W - stringWidth(prefix));
111
- console.log('');
112
- console.log(prefix + chalk.hex('#475569')('─'.repeat(remaining)));
151
+ console.log("");
152
+ console.log(prefix + chalk.hex("#334155")("─".repeat(remaining)));
113
153
  }
114
154
 
115
155
  /**
116
- * Nhãn bước tiến trình dạng pill badge: [ 1/6 ] Khởi tạo Git repo...
156
+ * Pill Step Badge
117
157
  */
118
158
  export function stepLabel(current, total, text) {
119
- const pill = chalk.bgHex('#3B82F6').hex('#FFFFFF').bold(` ${current}/${total} `);
120
- return `${pill} ` + chalk.hex('#E2E8F0')(text);
159
+ const pill = chalk
160
+ .bgHex("#2563EB")
161
+ .hex("#FFFFFF")
162
+ .bold(` ${current}/${total} `);
163
+ return `${pill} ` + chalk.hex("#F1F5F9").bold(text);
121
164
  }
122
165
 
123
166
  /**
124
- * Bảng tóm tắt thông tin triển khai trước khi push (Card bo góc sắc nét)
167
+ * Bảng tóm tắt thông tin trước khi Push — ấn tượng bằng chữ in hoa + pill màu.
125
168
  */
126
- export function printSummary({ repoUrl, commitMsg, dateDisplay, branch, force }) {
127
- const shortRepo = truncateVisual(repoUrl, 44);
128
- const shortMsg = truncateVisual(commitMsg, 44);
129
- const border = chalk.hex('#6366F1');
130
-
131
- const title = chalk.bold.hex('#F8FAFC')(' 📋 THÔNG TIN ĐẨY DỰ ÁN ');
132
- console.log('');
169
+ export function printSummary({
170
+ repoUrl,
171
+ commitMsg,
172
+ dateDisplay,
173
+ branch,
174
+ force,
175
+ sourceDir,
176
+ }) {
177
+ const border = chalk.hex("#475569");
178
+ const title = chalk.bold.bgHex("#4338CA").hex("#FFFFFF")(
179
+ " THÔNG TIN ĐẨY DỰ ÁN ",
180
+ );
181
+
182
+ console.log("");
133
183
  console.log(boxTop(title, border));
134
- console.log(boxRow(chalk.hex('#94A3B8')('🔗 Repository ') + chalk.hex('#38BDF8').underline(shortRepo), border));
135
- console.log(boxRow(chalk.hex('#94A3B8')('💬 Message ') + chalk.hex('#FBBF24').bold(shortMsg), border));
136
- console.log(boxRow(chalk.hex('#94A3B8')('📅 Thời gian ') + chalk.hex('#C084FC')(dateDisplay), border));
137
- console.log(boxRow(chalk.hex('#94A3B8')('🌿 Branch ') + chalk.bgHex('#0F766E').hex('#5EEAD4').bold(` ${branch} `), border));
184
+
185
+ if (sourceDir) {
186
+ console.log(
187
+ boxKvDot("Folder", chalk.hex("#34D399")(sourceDir), 14, border),
188
+ );
189
+ }
190
+ console.log(
191
+ boxKvDot(
192
+ "Repository",
193
+ chalk.hex("#38BDF8").underline(repoUrl),
194
+ 14,
195
+ border,
196
+ ),
197
+ );
198
+ console.log(
199
+ boxKvDot("Message", chalk.hex("#FBBF24").bold(commitMsg), 14, border),
200
+ );
201
+ console.log(
202
+ boxKvDot("Thời gian", chalk.hex("#C084FC")(dateDisplay), 14, border),
203
+ );
204
+
205
+ const branchPill = chalk
206
+ .bgHex("#065F46")
207
+ .hex("#6EE7B7")
208
+ .bold(` ${branch} `);
209
+ console.log(boxKvDot("Branch", branchPill, 14, border));
138
210
 
139
211
  const forceText = force
140
- ? chalk.bgHex('#991B1B').hex('#FCA5A5').bold(' BẬT (--force) ⚠️ ')
141
- : chalk.hex('#64748B')('Tắt (an toàn)');
142
- console.log(boxRow(chalk.hex('#94A3B8')('⚡ Force Push ') + forceText, border));
212
+ ? chalk.bgHex("#991B1B").hex("#FCA5A5").bold(" BẬT (--force) [!] ")
213
+ : chalk.hex("#64748B")("Tắt (an toàn)");
214
+ console.log(boxKvDot("Force Push", forceText, 14, border));
215
+
143
216
  console.log(boxBottom(border));
144
217
  }
145
218
 
219
+ export function printTargetDir(dir = "") {
220
+ const W = getW();
221
+ console.log(
222
+ chalk.hex("#64748B")(
223
+ ` » Folder: ${truncateVisual(String(dir || ""), W - 13)}`,
224
+ ),
225
+ );
226
+ }
227
+
146
228
  /**
147
- * Card cảnh báo file / nội dung secret trước khi commit (Theme Amber / Gold)
229
+ * Cảnh báo Folder lồng trong Repo cha — style chữ + viền hổ phách.
230
+ */
231
+ export function printNestedRepoWarning({ targetDir = "", topLevel = "" } = {}) {
232
+ const border = chalk.hex("#D97706");
233
+ const title = chalk.bold.bgHex("#D97706").hex("#FFFFFF")(
234
+ " FOLDER NẰM TRONG REPO CHA ",
235
+ );
236
+
237
+ console.log("");
238
+ console.log(boxTop(title, border));
239
+ console.log(
240
+ boxRow(
241
+ chalk.hex("#FDE047")(
242
+ "Folder nguồn đang nằm trong Git repo của folder cha:",
243
+ ),
244
+ border,
245
+ ),
246
+ );
247
+ console.log(boxKvDot(" +- CHA", chalk.hex("#F8FAFC")(topLevel), 10, border));
248
+ console.log(
249
+ boxKvDot(" +- CON", chalk.hex("#F8FAFC")(targetDir), 10, border),
250
+ );
251
+ console.log(boxRow("", border));
252
+ console.log(
253
+ boxRow(
254
+ chalk.hex("#94A3B8")(
255
+ "Nếu push tiếp, lịch sử/file của folder cha có thể đi kèm.",
256
+ ),
257
+ border,
258
+ ),
259
+ );
260
+ console.log(
261
+ boxRow(
262
+ chalk.hex("#34D399")(
263
+ "Gợi ý: Tạo repo RIÊNG để chỉ push folder này.",
264
+ ),
265
+ border,
266
+ ),
267
+ );
268
+ console.log(boxBottom(border));
269
+ }
270
+
271
+ /**
272
+ * Cảnh báo Secret nhạy cảm với cấu trúc cây (Tree View) — không dùng emoji.
148
273
  */
149
274
  export function printPrePushSecretWarning({ riskyFiles = [], findings = [] }) {
150
- const border = chalk.hex('#F59E0B');
151
- const title = chalk.bold.hex('#FEF08A')(' ⚠️ CẢNH BÁO: PHÁT HIỆN DỮ LIỆU NHẠY CẢM ');
275
+ const border = chalk.hex("#D97706");
276
+ const title = chalk.bold.bgHex("#D97706").hex("#FFFFFF")(
277
+ " PHÁT HIỆN DỮ LIỆU NHẠY CẢM ",
278
+ );
152
279
 
153
- console.log('');
280
+ console.log("");
154
281
  console.log(boxTop(title, border));
155
- console.log(boxRow(chalk.hex('#FDE047')('GitHub có thể chặn push nếu phát hiện key bí mật (GH013)'), border));
156
- console.log(boxRow('', border));
282
+ console.log(
283
+ boxRow(
284
+ chalk.hex("#FDE047")(
285
+ "GitHub có thể chặn push nếu phát hiện key bí mật (GH013)",
286
+ ),
287
+ border,
288
+ ),
289
+ );
290
+ console.log(boxRow("", border));
157
291
 
158
292
  if (riskyFiles.length > 0) {
159
- console.log(boxRow(chalk.bold.hex('#F8FAFC')('📁 File cấu hình nhạy cảm:'), border));
160
- riskyFiles.slice(0, 5).forEach((f, idx) => {
161
- const isLast = idx === riskyFiles.slice(0, 5).length - 1;
162
- const tree = chalk.hex('#64748B')(isLast ? ' └─ ' : ' ├─ ');
163
- console.log(boxRow(tree + chalk.hex('#FCD34D')(truncateVisual(f, 48)), border));
293
+ console.log(
294
+ boxRow(chalk.bold.hex("#F8FAFC")("File cấu hình nguy cơ:"), border),
295
+ );
296
+ const showFiles = riskyFiles.slice(0, 4);
297
+ showFiles.forEach((f, idx) => {
298
+ const isLast = idx === showFiles.length - 1 && findings.length === 0;
299
+ const tree = chalk.hex("#64748B")(isLast ? " +- " : " +- ");
300
+ console.log(
301
+ boxRow(tree + chalk.hex("#FCD34D")(truncateVisual(f, 46)), border),
302
+ );
164
303
  });
165
- if (riskyFiles.length > 5) {
166
- console.log(boxRow(chalk.hex('#94A3B8')(` └─ …và ${riskyFiles.length - 5} file khác`), border));
304
+ if (riskyFiles.length > 4) {
305
+ console.log(
306
+ boxRow(
307
+ chalk.hex("#64748B")(` +- ...và ${riskyFiles.length - 4} file khác`),
308
+ border,
309
+ ),
310
+ );
167
311
  }
168
312
  }
169
313
 
170
314
  if (findings.length > 0) {
171
- if (riskyFiles.length > 0) console.log(boxRow('', border));
172
- console.log(boxRow(chalk.bold.hex('#F8FAFC')('🔑 Vị trí nghi vấn secret:'), border));
173
- findings.slice(0, 5).forEach((f, idx) => {
174
- const isLast = idx === findings.slice(0, 5).length - 1;
175
- const tree = chalk.hex('#64748B')(isLast ? ' └─ ' : ' ├─ ');
176
- const loc = truncateVisual(`${f.file}:${f.line}`, 28);
177
- console.log(boxRow(tree + chalk.hex('#F8FAFC')(loc) + chalk.hex('#94A3B8')(` [${f.type}]`), border));
315
+ if (riskyFiles.length > 0) console.log(boxRow("", border));
316
+ console.log(
317
+ boxRow(
318
+ chalk.bold.hex("#F8FAFC")("Vị trí nghi vấn Secret trong code:"),
319
+ border,
320
+ ),
321
+ );
322
+ const showFindings = findings.slice(0, 4);
323
+ showFindings.forEach((f, idx) => {
324
+ const isLast = idx === showFindings.length - 1;
325
+ const tree = chalk.hex("#64748B")(isLast ? " +- " : " +- ");
326
+ const loc = truncateVisual(`${f.file}:${f.line}`, 26);
327
+ console.log(
328
+ boxRow(
329
+ tree +
330
+ chalk.hex("#F8FAFC")(loc) +
331
+ chalk.hex("#94A3B8")(` [${f.type}]`),
332
+ border,
333
+ ),
334
+ );
178
335
  if (f.preview) {
179
- const indent = chalk.hex('#64748B')(isLast ? ' ' : ' │ ');
180
- console.log(boxRow(indent + chalk.hex('#64748B')(`"${truncateVisual(f.preview, 44)}"`), border));
336
+ const indent = chalk.hex("#64748B")(isLast ? " " : " | ");
337
+ console.log(
338
+ boxRow(
339
+ indent + chalk.hex("#64748B")(`"${truncateVisual(f.preview, 40)}"`),
340
+ border,
341
+ ),
342
+ );
181
343
  }
182
344
  });
183
- if (findings.length > 5) {
184
- console.log(boxRow(chalk.hex('#94A3B8')(` └─ …và ${findings.length - 5} vị trí khác`), border));
345
+ if (findings.length > 4) {
346
+ console.log(
347
+ boxRow(
348
+ chalk.hex("#64748B")(` +- ...và ${findings.length - 4} vị trí khác`),
349
+ border,
350
+ ),
351
+ );
185
352
  }
186
353
  }
187
354
 
188
- console.log(boxRow('', border));
189
- console.log(boxRow(chalk.hex('#A7F3D0')(truncateVisual('💡 Khuyên dùng: Chọn "Tự động fix" để gỡ file + .gitignore', W - 4)), border));
355
+ console.log(boxRow("", border));
356
+ console.log(
357
+ boxRow(
358
+ chalk.hex("#34D399")(
359
+ 'Gợi ý: Chọn "Tự động fix" để gỡ file + .gitignore',
360
+ ),
361
+ border,
362
+ ),
363
+ );
190
364
  console.log(boxBottom(border));
191
365
  }
192
366
 
193
367
  /**
194
- * Card cảnh báo khi push bị GitHub Push Protection chặn (Theme Crimson / Ruby)
368
+ * Cảnh báo Push bị chặn bởi GitHub Push Protection — style chữ in hoa.
195
369
  */
196
370
  export function printSecretBlockedBox(rawMessage, details = {}) {
197
- const border = chalk.hex('#EF4444');
371
+ const border = chalk.hex("#E11D48");
198
372
  const files = details.files || [];
199
373
  const types = details.secretTypes || [];
200
- const title = chalk.bold.hex('#FEE2E2')(' 🚫 PUSH BỊ CHẶN — PHÁT HIỆN SECRET (GH013) ');
374
+ const title = chalk.bold.bgHex("#E11D48").hex("#FFFFFF")(
375
+ " PUSH BỊ CHẶN — PHÁT HIỆN SECRET (GH013) ",
376
+ );
201
377
 
202
- console.log('');
378
+ console.log("");
203
379
  console.log(boxTop(title, border));
204
- console.log(boxRow(chalk.hex('#FCA5A5')('GitHub Push Protection đã chặn push vì phát hiện secret!'), border));
205
- console.log(boxRow('', border));
380
+ console.log(
381
+ boxRow(
382
+ chalk.hex("#FDA4AF")(
383
+ "GitHub Push Protection đã chặn push vì phát hiện secret!",
384
+ ),
385
+ border,
386
+ ),
387
+ );
388
+ console.log(boxRow("", border));
206
389
 
207
390
  if (types.length > 0) {
208
- console.log(boxRow(chalk.hex('#94A3B8')('🔑 Loại secret : ') + chalk.hex('#F8FAFC').bold(types.slice(0, 2).join(', ')), border));
391
+ console.log(
392
+ boxKvDot(
393
+ "Loại secret",
394
+ chalk.hex("#F8FAFC").bold(types.slice(0, 2).join(", ")),
395
+ 14,
396
+ border,
397
+ ),
398
+ );
209
399
  }
210
400
  if (files.length > 0) {
211
- files.slice(0, 3).forEach((f) => {
212
- console.log(boxRow(chalk.hex('#94A3B8')('📁 File nguồn : ') + chalk.hex('#FCD34D')(truncateVisual(f, 44)), border));
401
+ files.slice(0, 2).forEach((f) => {
402
+ console.log(
403
+ boxKvDot("File nguồn", chalk.hex("#FCD34D")(f), 14, border),
404
+ );
213
405
  });
214
406
  }
215
407
 
216
- console.log(boxRow('', border));
217
- console.log(boxRow(chalk.bold.hex('#F8FAFC')('👉 Các giải pháp khắc phục:'), border));
218
- console.log(boxRow(chalk.hex('#F8FAFC')(' ① Thu hồi (revoke) key ngay nếu là token thật'), border));
219
- console.log(boxRow(chalk.hex('#38BDF8')(' ② Chạy: git-push-time fix để tự động gỡ & squash commit'), border));
220
- console.log(boxRow(chalk.hex('#94A3B8')(' ③ Xóa secret trong code / dùng biến môi trường (process.env)'), border));
221
- console.log(boxRow(chalk.hex('#94A3B8')(' ④ Sau đó chạy lại tool để đẩy code lên'), border));
408
+ console.log(boxRow("", border));
409
+ console.log(
410
+ boxRow(chalk.bold.hex("#F8FAFC")("» Các giải pháp khắc phục:"), border),
411
+ );
412
+ console.log(
413
+ boxRow(
414
+ chalk.hex("#E2E8F0")(" [1] Thu hồi (revoke) key ngay nếu là token thật"),
415
+ border,
416
+ ),
417
+ );
418
+ console.log(
419
+ boxRow(
420
+ chalk.hex("#38BDF8")(
421
+ " [2] Chạy: git-push-time fix để tự động gỡ & squash commit",
422
+ ),
423
+ border,
424
+ ),
425
+ );
426
+ console.log(
427
+ boxRow(
428
+ chalk.hex("#94A3B8")(
429
+ " [3] Xóa secret trong code / dùng biến môi trường (process.env)",
430
+ ),
431
+ border,
432
+ ),
433
+ );
434
+ console.log(
435
+ boxRow(
436
+ chalk.hex("#94A3B8")(" [4] Sau đó chạy lại tool để đẩy code lên"),
437
+ border,
438
+ ),
439
+ );
222
440
 
223
441
  console.log(boxBottom(border));
224
442
 
225
443
  if (details.unblockUrl) {
226
- console.log(chalk.hex('#F59E0B')(`\n 🔓 URL mở khóa tạm thời (nếu là key mẫu thử nghiệm):\n ${chalk.underline(details.unblockUrl)}`));
444
+ console.log(
445
+ chalk.hex("#F59E0B")(
446
+ `\n » URL mở khóa tạm thời (nếu là key mẫu thử nghiệm):\n ${chalk.underline(details.unblockUrl)}`,
447
+ ),
448
+ );
227
449
  }
228
- console.log(chalk.hex('#64748B')(`\n 📖 Tài liệu GitHub: ${details.resolveUrl || 'https://docs.github.com/code-security/secret-scanning/pushing-a-branch-blocked-by-push-protection'}\n`));
450
+ console.log(
451
+ chalk.hex("#64748B")(
452
+ `\n » Tài liệu GitHub: ${details.resolveUrl || "https://docs.github.com/code-security/secret-scanning/pushing-a-branch-blocked-by-push-protection"}\n`,
453
+ ),
454
+ );
229
455
  }
230
456
 
231
457
  /**
232
- * Card thông báo thành công rực rỡ (Theme Emerald / Mint Green)
458
+ * Card Thông báo Thành công — ấn tượng bằng viền xanh + chữ in hoa.
233
459
  */
234
460
  export function printSuccessBox({ branch, repoUrl }) {
235
- const border = chalk.hex('#10B981');
236
- const title = chalk.bold.hex('#ECFDF5')(' 🎉 ĐẨY DỰ ÁN LÊN GIT THÀNH CÔNG! ');
461
+ const border = chalk.hex("#059669");
462
+ const title = chalk.bold.bgHex("#10B981").hex("#064E3B")(
463
+ " ĐẨY DỰ ÁN LÊN GIT THÀNH CÔNG! ",
464
+ );
237
465
 
238
- console.log('');
466
+ console.log("");
239
467
  console.log(boxTop(title, border));
240
- console.log(boxRow('', border));
241
- console.log(boxRow(chalk.hex('#94A3B8')('🌿 Branch : ') + chalk.bgHex('#065F46').hex('#6EE7B7').bold(` ${branch} `), border));
242
- console.log(boxRow(chalk.hex('#94A3B8')('🔗 Remote : ') + chalk.hex('#38BDF8').underline(truncateVisual(repoUrl, 44)), border));
243
- console.log(boxRow(chalk.hex('#94A3B8')('✨ Kết quả : ') + chalk.hex('#A7F3D0')('Mọi thay đổi đã được đồng bộ lên Git!'), border));
244
- console.log(boxRow('', border));
245
- console.log(boxRow(chalk.hex('#64748B')('💡 Kiểm tra lịch sử commit: git log -1 --stat'), border));
468
+ console.log(boxRow("", border));
469
+
470
+ const branchPill = chalk.bgHex("#065F46").hex("#6EE7B7").bold(` ${branch} `);
471
+ console.log(boxKvDot("Branch", branchPill, 12, border));
472
+ console.log(
473
+ boxKvDot("Remote", chalk.hex("#38BDF8").underline(repoUrl), 12, border),
474
+ );
475
+ console.log(
476
+ boxKvDot(
477
+ "Trạng thái",
478
+ chalk.hex("#A7F3D0")("Đồng bộ hoàn tất 100%"),
479
+ 12,
480
+ border,
481
+ ),
482
+ );
483
+
484
+ console.log(boxRow("", border));
485
+ console.log(
486
+ boxRow(
487
+ chalk.hex("#64748B")("» Kiểm tra lịch sử commit: git log -1 --stat"),
488
+ border,
489
+ ),
490
+ );
246
491
  console.log(boxBottom(border));
247
- console.log('');
492
+ console.log("");
248
493
  }
249
494
 
250
- /**
251
- * Thông báo hủy thao tác
252
- */
253
495
  export function printCancelled() {
254
- const border = chalk.hex('#F59E0B');
255
- const title = chalk.bold.hex('#FEF3C7')(' ✋ ĐÃ HỦY THAO TÁC ');
256
- console.log('');
496
+ const border = chalk.hex("#D97706");
497
+ const title = chalk.bold.bgHex("#D97706").hex("#FFFFFF")(
498
+ " ĐÃ HỦY THAO TÁC ",
499
+ );
500
+ console.log("");
257
501
  console.log(boxTop(title, border));
258
- console.log(boxRow(chalk.hex('#FDE68A')('Không có gì thay đổi trên kho lưu trữ.'), border));
502
+ console.log(
503
+ boxRow(
504
+ chalk.hex("#FDE68A")(
505
+ "Không có bất kỳ thay đổi nào được thực hiện trên repo.",
506
+ ),
507
+ border,
508
+ ),
509
+ );
259
510
  console.log(boxBottom(border));
260
- console.log('');
511
+ console.log("");
261
512
  }
262
513
 
263
- /**
264
- * Card lỗi chung (Theme Rose / Red) — dùng cho mọi lỗi validation, git, npm
265
- */
266
- export function printErrorBox(title = 'XẢY RA LỖI', lines = [], hint = '') {
267
- const border = chalk.hex('#EF4444');
268
- const titleStyled = chalk.bold.hex('#FEE2E2')(` ❌ ${title} `);
269
- const rows = Array.isArray(lines) ? lines : [String(lines || '')];
514
+ export function printErrorBox(title = "XẢY RA LỖI", lines = [], hint = "") {
515
+ const W = getW();
516
+ const border = chalk.hex("#E11D48");
517
+ const cleanTitle = String(title || "").toUpperCase();
518
+ const titleStyled = chalk.bold.bgHex("#E11D48").hex("#FFFFFF")(
519
+ ` [LỖI] ${cleanTitle} `,
520
+ );
521
+ const rows = Array.isArray(lines) ? lines : [String(lines || "")];
270
522
 
271
- console.log('');
523
+ console.log("");
272
524
  console.log(boxTop(titleStyled, border));
273
525
  rows.forEach((l) => {
274
526
  if (!l) {
275
- console.log(boxRow('', border));
527
+ console.log(boxRow("", border));
276
528
  } else {
277
- console.log(boxRow(chalk.hex('#FCA5A5')(truncateVisual(String(l), W - 4)), border));
529
+ console.log(
530
+ boxRow(chalk.hex("#FDA4AF")(truncateVisual(String(l), W - 4)), border),
531
+ );
278
532
  }
279
533
  });
280
534
  if (hint) {
281
- console.log(boxRow('', border));
282
- console.log(boxRow(chalk.hex('#38BDF8')(`👉 ${truncateVisual(hint, W - 8)}`), border));
535
+ console.log(boxRow("", border));
536
+ console.log(
537
+ boxRow(chalk.hex("#38BDF8")(`» ${truncateVisual(hint, W - 6)}`), border),
538
+ );
283
539
  }
284
540
  console.log(boxBottom(border));
285
- console.log('');
541
+ console.log("");
286
542
  }
287
543
 
288
- /**
289
- * Lỗi chưa cài Git
290
- */
291
544
  export function printGitMissing() {
292
- printErrorBox('CHƯA CÀI ĐẶT GIT', [
293
- 'Chưa phát hiện Git trên máy. Hãy cài Git rồi chạy lại tool nhé!',
294
- ], 'https://git-scm.com/downloads');
545
+ printErrorBox(
546
+ "CHƯA CÀI ĐẶT GIT",
547
+ [
548
+ "Không tìm thấy Git trên hệ thống.",
549
+ "Vui lòng tải và cài đặt Git để tiếp tục!",
550
+ ],
551
+ "Tải ngay tại: https://git-scm.com/downloads",
552
+ );
295
553
  }
296
554
 
297
- /**
298
- * Lỗi xác thực Git (sai token / hết hạn / chưa login)
299
- */
300
- export function printAuthFailedBox(message = '') {
301
- const border = chalk.hex('#EF4444');
302
- const title = chalk.bold.hex('#FEE2E2')(' 🔒 LỖI XÁC THỰC GIT ');
303
- const firstLine = String(message || '').split('\n')[0].slice(0, 120);
555
+ export function printAuthFailedBox(message = "") {
556
+ const border = chalk.hex("#E11D48");
557
+ const title = chalk.bold.bgHex("#E11D48").hex("#FFFFFF")(
558
+ " LỖI XÁC THỰC GIT ",
559
+ );
560
+ const firstLine = String(message || "")
561
+ .split("\n")[0]
562
+ .slice(0, 120);
304
563
 
305
- console.log('');
564
+ console.log("");
306
565
  console.log(boxTop(title, border));
307
- console.log(boxRow(chalk.hex('#FCA5A5')('Không thể xác thực với remote. Kiểm tra lại quyền truy cập.'), border));
308
- if (firstLine) console.log(boxRow(chalk.hex('#64748B')(truncateVisual(firstLine, W - 4)), border));
309
- console.log(boxRow('', border));
310
- console.log(boxRow(chalk.bold.hex('#F8FAFC')('👉 Cách khắc phục nhanh:'), border));
311
- console.log(boxRow(chalk.hex('#F8FAFC')(' ① gh auth login (nếu dùng GitHub CLI)'), border));
312
- console.log(boxRow(chalk.hex('#F8FAFC')(' ② Tạo Personal Access Token mới rồi push lại'), border));
313
- console.log(boxRow(chalk.hex('#94A3B8')(' ③ Dùng SSH: git@github.com:user/repo.git'), border));
566
+ console.log(
567
+ boxRow(
568
+ chalk.hex("#FDA4AF")(
569
+ "Không thể xác thực với remote. Kiểm tra lại quyền truy cập.",
570
+ ),
571
+ border,
572
+ ),
573
+ );
574
+ if (firstLine)
575
+ console.log(
576
+ boxRow(
577
+ chalk.hex("#64748B")(truncateVisual(firstLine, getW() - 4)),
578
+ border,
579
+ ),
580
+ );
581
+ console.log(boxRow("", border));
582
+ console.log(
583
+ boxRow(chalk.bold.hex("#F8FAFC")("» Cách khắc phục nhanh:"), border),
584
+ );
585
+ console.log(
586
+ boxRow(
587
+ chalk.hex("#E2E8F0")(" [1] gh auth login (Nếu dùng GitHub CLI)"),
588
+ border,
589
+ ),
590
+ );
591
+ console.log(
592
+ boxRow(
593
+ chalk.hex("#E2E8F0")(" [2] Tạo Personal Access Token mới rồi push lại"),
594
+ border,
595
+ ),
596
+ );
597
+ console.log(
598
+ boxRow(
599
+ chalk.hex("#94A3B8")(" [3] Sử dụng SSH: git@github.com:user/repo.git"),
600
+ border,
601
+ ),
602
+ );
314
603
  console.log(boxBottom(border));
315
- console.log('');
604
+ console.log("");
316
605
  }
317
606
 
318
- /**
319
- * Lỗi remote có commit mới hơn local (non-fast-forward)
320
- */
321
- export function printNonFastForwardBox(message = '', branch = 'main') {
322
- const border = chalk.hex('#F59E0B');
323
- const title = chalk.bold.hex('#FEF3C7')(' ⚠️ REMOTE ĐÃ CÓ COMMIT MỚI HƠN ');
324
- const firstLine = String(message || '').split('\n')[0].slice(0, 120);
607
+ export function printNonFastForwardBox(message = "", branch = "main") {
608
+ const border = chalk.hex("#D97706");
609
+ const title = chalk.bold.bgHex("#D97706").hex("#FFFFFF")(
610
+ " REMOTE ĐÃ CÓ COMMIT MỚI HƠN ",
611
+ );
612
+ const firstLine = String(message || "")
613
+ .split("\n")[0]
614
+ .slice(0, 120);
325
615
 
326
- console.log('');
616
+ console.log("");
327
617
  console.log(boxTop(title, border));
328
- console.log(boxRow(chalk.hex('#FDE68A')('Local của bạn đang tụt hậu so với remote.'), border));
329
- if (firstLine) console.log(boxRow(chalk.hex('#64748B')(truncateVisual(firstLine, W - 4)), border));
330
- console.log(boxRow('', border));
331
- console.log(boxRow(chalk.hex('#38BDF8')(` → Chạy: git pull --rebase origin ${branch} rồi push lại`), border));
332
- console.log(boxRow(chalk.hex('#94A3B8')(' Chỉ dùng --force khi chắc chắn muốn ghi đè.'), border));
618
+ console.log(
619
+ boxRow(
620
+ chalk.hex("#FDE68A")(
621
+ "Kho lưu trữ trên Remote đang chứa commit mới hơn Local.",
622
+ ),
623
+ border,
624
+ ),
625
+ );
626
+ if (firstLine)
627
+ console.log(
628
+ boxRow(
629
+ chalk.hex("#64748B")(truncateVisual(firstLine, getW() - 4)),
630
+ border,
631
+ ),
632
+ );
633
+ console.log(boxRow("", border));
634
+ console.log(
635
+ boxRow(
636
+ chalk.hex("#38BDF8")(
637
+ ` » Chạy: git pull --rebase origin ${branch} rồi push lại`,
638
+ ),
639
+ border,
640
+ ),
641
+ );
642
+ console.log(
643
+ boxRow(
644
+ chalk.hex("#94A3B8")(
645
+ " Chỉ dùng --force khi bạn chắc chắn muốn ghi đè Remote.",
646
+ ),
647
+ border,
648
+ ),
649
+ );
333
650
  console.log(boxBottom(border));
334
- console.log('');
651
+ console.log("");
335
652
  }
336
653
 
337
- /**
338
- * Kết quả auto-fix secret (dùng chung cho flow push + lệnh fix)
339
- */
340
- export function printFixResult({ fixed = [], gitignoreAdded = [], codeFilesWithSecrets = [], failed = [] } = {}) {
654
+ export function printFixResult({
655
+ fixed = [],
656
+ gitignoreAdded = [],
657
+ codeFilesWithSecrets = [],
658
+ failed = [],
659
+ } = {}) {
660
+ const W = getW();
661
+ console.log("");
341
662
  if (fixed.length > 0) {
342
- console.log(chalk.hex('#34D399')(` ✓ Đã gỡ ${fixed.length} file khỏi commit: ${truncateVisual(fixed.join(', '), 46)}`));
663
+ console.log(
664
+ chalk.hex("#34D399")(
665
+ ` [OK] Đã gỡ ${fixed.length} file khỏi commit: ${truncateVisual(fixed.join(", "), W - 32)}`,
666
+ ),
667
+ );
343
668
  }
344
669
  if (gitignoreAdded.length > 0) {
345
- console.log(chalk.cyan(` + .gitignore: ${truncateVisual(gitignoreAdded.join(', '), 48)}`));
670
+ console.log(
671
+ chalk.cyan(
672
+ ` [+] Thêm vào .gitignore: ${truncateVisual(gitignoreAdded.join(", "), W - 28)}`,
673
+ ),
674
+ );
346
675
  }
347
676
  if (codeFilesWithSecrets && codeFilesWithSecrets.length > 0) {
348
- console.log(chalk.yellowBright(' ⚠️ File mã nguồn chứa secret trong code (đã gỡ khỏi commit, KHÔNG đưa vào .gitignore):'));
349
- codeFilesWithSecrets.forEach((f) => console.log(chalk.yellow(` • ${f}`)));
350
- console.log(chalk.gray(' 👉 Mở file để xóa key, thay bằng biến môi trường (process.env).'));
677
+ console.log(
678
+ chalk.yellowBright(
679
+ " [!] File code chứa secret (đã gỡ khỏi commit, KHÔNG cho vào .gitignore):",
680
+ ),
681
+ );
682
+ codeFilesWithSecrets.forEach((f) =>
683
+ console.log(chalk.yellow(` - ${f}`)),
684
+ );
685
+ console.log(
686
+ chalk.gray(" » Hãy xóa key trong file và thay bằng process.env."),
687
+ );
351
688
  }
352
689
  if (failed.length > 0) {
353
- failed.forEach((f) => console.log(chalk.red(` ✗ ${f.file}: ${f.reason}`)));
354
- }
355
- }
356
-
357
- /**
358
- * Card npm publish (Theme Violet)
359
- */
360
- export function printNpmResult({ ok, pkgName, version, output = '', hint = '', dryRun = false }) {
361
- if (ok) {
362
- const border = chalk.hex('#8B5CF6');
363
- const title = chalk.bold.hex('#EDE9FE')(dryRun ? ' 🧪 DRY-RUN OK — SẴN SÀNG PUBLISH ' : ' 📦 PUBLISH NPM THÀNH CÔNG! ');
364
- console.log('');
365
- console.log(boxTop(title, border));
366
- console.log(boxRow(chalk.hex('#94A3B8')('📦 Package : ') + chalk.hex('#F8FAFC').bold(`${pkgName}@${version}`), border));
367
- if (!dryRun) {
368
- console.log(boxRow(chalk.hex('#94A3B8')('🔗 Xem tại : ') + chalk.hex('#38BDF8').underline(`npmjs.com/package/${pkgName}`), border));
369
- } else {
370
- console.log(boxRow(chalk.hex('#94A3B8')('✨ Kết quả : ') + chalk.hex('#A7F3D0')('Không lỗi — chạy thật để publish.'), border));
371
- }
372
- console.log(boxBottom(border));
373
- console.log('');
374
- return;
690
+ failed.forEach((f) => console.log(chalk.red(` [x] ${f.file}: ${f.reason}`)));
375
691
  }
376
- printErrorBox('PUBLISH NPM THẤT BẠI', output.split('\n').slice(-6).filter(Boolean), hint);
377
692
  }
378
693
 
379
- /**
380
- * Footer nhỏ sau mỗi lần chạy thành công / kết thúc
381
- */
382
694
  export function printFooter() {
383
- console.log(chalk.hex('#475569')(' ── ✦ ──'));
384
- console.log(chalk.hex('#64748B')(' 💡 Mẹo: dùng ') + chalk.hex('#38BDF8')('git-push-time -y --fix --publish') + chalk.hex('#64748B')(' để auto 100% cho lần sau.'));
385
- console.log(chalk.hex('#334155')(' 📖 Docs: https://github.com — Issues: tạo issue nếu gặp lỗi push.\n'));
695
+ console.log(chalk.hex("#334155")(" ─── » ───"));
696
+ console.log(
697
+ chalk.hex("#64748B")(" » Mẹo: Dùng ") +
698
+ chalk.hex("#38BDF8").bold("git-push-time -y --fix") +
699
+ chalk.hex("#64748B")(" để tự động hóa 100%."),
700
+ );
701
+ console.log(
702
+ chalk.hex("#475569")(
703
+ " » Docs: https://github.com — Chúc bạn làm việc hiệu quả!\n",
704
+ ),
705
+ );
386
706
  }
387
707
 
388
- /**
389
- * Style cho --help của commander cho đồng bộ theme
390
- */
391
708
  export function styleCliHelp(program) {
392
709
  program.configureHelp({
393
- styleTitle: (t) => chalk.bold.hex('#38BDF8')(t),
394
- styleCommandText: (t) => chalk.hex('#F8FAFC')(t),
395
- styleOptionText: (t) => chalk.hex('#5EEAD4')(t),
396
- styleArgumentText: (t) => chalk.hex('#FBBF24')(t),
397
- styleDescriptionText: (t) => chalk.hex('#94A3B8')(t),
710
+ styleTitle: (t) => chalk.bold.hex("#38BDF8")(t),
711
+ styleCommandText: (t) => chalk.hex("#F8FAFC")(t),
712
+ styleOptionText: (t) => chalk.hex("#34D399")(t),
713
+ styleArgumentText: (t) => chalk.hex("#FBBF24")(t),
714
+ styleDescriptionText: (t) => chalk.hex("#94A3B8")(t),
398
715
  });
399
- program.addHelpText('after', `
400
- ${chalk.hex('#475569')('─── ✦ Ví dụ nhanh ───')}
401
- ${chalk.hex('#34D399')('$')} ${chalk.white('git-push-time -r https://github.com/user/repo.git -m "Init" -d "-2d" -y')}
402
- ${chalk.hex('#34D399')('$')} ${chalk.white('git-push-time fix')}
403
- ${chalk.hex('#34D399')('$')} ${chalk.white('git-push-time -r <url> --publish --dry-run')}
404
- `);
716
+ program.addHelpText(
717
+ "after",
718
+ `
719
+ ${chalk.hex("#334155")("─── » Ví dụ nhanh ───")}
720
+ ${chalk.hex("#34D399")("$")} ${chalk.white('git-push-time -r https://github.com/user/repo.git -m "Init" -d "-2d" -y')}
721
+ ${chalk.hex("#34D399")("$")} ${chalk.white("git-push-time fix")}
722
+ `,
723
+ );
405
724
  }
406
725
 
407
- export function line(char = '─') {
408
- return chalk.hex('#334155')(char.repeat(W));
726
+ export function line(char = "─") {
727
+ return chalk.hex("#334155")(char.repeat(getW()));
409
728
  }