git-push-time 1.1.1 → 1.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/ui.js CHANGED
@@ -1,16 +1,24 @@
1
1
  import chalk from "chalk";
2
+ import boxen from "boxen";
2
3
  import stringWidth from "string-width";
3
4
 
4
5
  /**
5
- * Tự động tính chiều rộng card theo Terminal, có giới hạn cho cửa sổ rộng.
6
+ * Chiều rộng khung thống nhất cho toàn bộ UI.
7
+ * - Luôn bằng nhau giữa dashboard / banner / prompt / box chi tiết
8
+ * nên các khung xếp chồng không bị lệch (vỡ giao diện).
9
+ * - Terminal hẹp thì co theo `cols - 2`, terminal rộng thì cap 76.
6
10
  */
7
11
  export function getW() {
8
- const cols = process.stdout.columns || 72;
9
- return Math.min(76, Math.max(24, cols - 4));
12
+ const cols = process.stdout.columns;
13
+ if (!cols || cols < 10) return 72;
14
+ const avail = cols - 2;
15
+ // Terminal quá hẹp (<34): dùng đúng phần hiển thị được, tránh tràn dòng.
16
+ if (avail < 34) return Math.max(24, avail);
17
+ return Math.min(76, avail);
10
18
  }
11
19
 
12
20
  function fitStyled(text, maxWidth) {
13
- const value = String(text ?? '');
21
+ const value = String(text ?? '').replace(/[\r\n\t]+/g, ' ');
14
22
  if (maxWidth <= 0) return '';
15
23
  if (stringWidth(value) <= maxWidth) return value;
16
24
 
@@ -73,58 +81,93 @@ export function truncateVisual(str, maxLen) {
73
81
  return fitStyled(str, maxLen);
74
82
  }
75
83
 
84
+ /** Khung tiêu đề nhất quán cho từng bước tương tác trong terminal. */
85
+ export function framePrompt(message) {
86
+ // Dùng cùng getW() với mọi box khác để các khung luôn thẳng hàng,
87
+ // không còn tình trạng prompt hẹp (56) còn dashboard rộng (76) gây vỡ.
88
+ return boxen(String(message ?? ""), {
89
+ borderStyle: "round",
90
+ borderColor: "#818CF8",
91
+ padding: { left: 1, right: 1, top: 0, bottom: 0 },
92
+ width: getW(),
93
+ textAlignment: "center",
94
+ });
95
+ }
96
+
76
97
  /* ==========================================================================
77
98
  BOX ENGINE CHUẨN (Bo góc mềm & Căn lề pixel-perfect)
78
99
  ========================================================================== */
79
100
 
80
101
  export function boxTop(title = "", borderColor = chalk.hex("#334155")) {
81
102
  const W = getW();
82
- if (!title) return borderColor("╭" + "─".repeat(W - 2) + "╮");
103
+ if (!title) return borderColor("╭" + "─".repeat(Math.max(2, W - 2)) + "╮");
83
104
 
84
- const fittedTitle = fitStyled(title, W - 8);
105
+ // Căn giữa tiêu đề để 2 bên đều nhau: ╭──── Title ────╮
106
+ // Chừa 1 space mỗi bên title + tối thiểu 2 gạch mỗi bên.
107
+ const maxTitle = Math.max(4, W - 8);
108
+ const fittedTitle = fitStyled(title, maxTitle);
85
109
  const titleWidth = stringWidth(fittedTitle);
86
- const leftDash = "─".repeat(3);
87
- const rightDashLen = Math.max(0, W - 2 - 3 - titleWidth);
110
+ const remaining = Math.max(4, W - 2 - titleWidth - 2);
111
+ const leftLen = Math.ceil(remaining / 2);
112
+ const rightLen = Math.floor(remaining / 2);
88
113
  return (
89
- borderColor("╭" + leftDash) +
114
+ borderColor("╭" + "─".repeat(leftLen) + " ") +
90
115
  fittedTitle +
91
- borderColor("─".repeat(rightDashLen) + "╮")
116
+ borderColor(" " + "─".repeat(rightLen) + "╮")
92
117
  );
93
118
  }
94
119
 
120
+ /** Khung tiêu đề căn trái theo lề nội dung của boxRow. */
121
+ export function boxTopLeft(title = "", borderColor = chalk.hex("#334155")) {
122
+ const W = getW();
123
+ if (!title) return borderColor("╭" + "─".repeat(Math.max(2, W - 2)) + "╮");
124
+ const fittedTitle = truncateVisual(title, W - 6);
125
+ const titleWidth = stringWidth(fittedTitle);
126
+ const rightDash = Math.max(0, W - titleWidth - 4);
127
+ return borderColor("╭ ") + fittedTitle + borderColor(" " + "─".repeat(rightDash) + "╮");
128
+ }
129
+
95
130
  export function boxBottom(borderColor = chalk.hex("#334155")) {
96
131
  const W = getW();
97
- return borderColor("╰" + "─".repeat(W - 2) + "╯");
132
+ return borderColor("╰" + "─".repeat(Math.max(2, W - 2)) + "╯");
98
133
  }
99
134
 
100
135
  export function boxRow(content = "", borderColor = chalk.hex("#334155")) {
101
136
  const W = getW();
102
- return borderColor("│ ") + pad(fitStyled(content, W - 4), W - 4) + borderColor(" │");
137
+ const inner = Math.max(4, W - 4);
138
+ return borderColor("│ ") + pad(fitStyled(content, inner), inner) + borderColor(" │");
103
139
  }
104
140
 
105
141
  export function boxDivider(borderColor = chalk.hex("#334155")) {
106
142
  const W = getW();
107
- return borderColor("├" + "─".repeat(W - 2) + "┤");
143
+ return borderColor("├" + "─".repeat(Math.max(2, W - 2)) + "┤");
108
144
  }
109
145
 
110
146
  /**
111
- * Hàng Key-Value có đường nối bằng dấu chấm ․ ․ ․
147
+ * Hàng Key-Value có đường nối bằng dấu chấm · · ·
148
+ * Dùng "·" (U+00B7) thay vì "․" (U+2024) để không bị vỡ font trên
149
+ * Windows PowerShell / Consolas. Label không còn bị cắt oan khi
150
+ * terminal hẹp nhờ công thức fitted mới.
151
+ */
152
+ /**
153
+ * Hàng Key-Value có đường nối bằng dấu chấm · · ·
154
+ * Căn lề chuẩn xác, khoảng cách đều và không bị vỡ font trên mọi terminal.
112
155
  */
113
156
  export function boxKvDot(
114
157
  label,
115
158
  value,
116
- labelWidth = 16,
159
+ labelWidth = 14,
117
160
  borderColor = chalk.hex("#334155"),
118
161
  ) {
119
162
  const W = getW();
120
- const innerWidth = W - 4;
121
- const fittedLabelWidth = Math.min(labelWidth, Math.max(8, Math.floor(innerWidth * 0.4)));
122
- const maxValWidth = Math.max(4, innerWidth - fittedLabelWidth - 1);
163
+ const innerWidth = Math.max(8, W - 4);
164
+ const fittedLabelWidth = Math.min(labelWidth, Math.max(4, innerWidth - 10));
165
+ const maxValWidth = Math.max(4, innerWidth - fittedLabelWidth - 4);
123
166
  const truncatedVal = truncateVisual(value, maxValWidth);
124
167
  const valWidth = stringWidth(truncatedVal);
125
168
 
126
- const dotsLen = Math.max(1, innerWidth - fittedLabelWidth - valWidth);
127
- const dots = chalk.hex("#1E293B")("․".repeat(dotsLen));
169
+ const dotsLen = Math.max(2, innerWidth - fittedLabelWidth - valWidth - 2);
170
+ const dots = " " + chalk.hex("#334155")("·".repeat(dotsLen)) + " ";
128
171
  const formattedLabel = chalk.hex("#94A3B8")(
129
172
  pad(fitStyled(label, fittedLabelWidth), fittedLabelWidth),
130
173
  );
@@ -136,66 +179,204 @@ export function boxKvDot(
136
179
  PUBLIC INTERFACE & BANNER
137
180
  ========================================================================== */
138
181
 
182
+ /* ==========================================================================
183
+ CYBER CAT MASCOT — 20x12 pixel art grid, two pixel rows per terminal cell
184
+ ========================================================================== */
185
+
186
+ const MASCOT_COLORS = {
187
+ P: "#818CF8", // Indigo ears
188
+ W: "#F8FAFC", // White face
189
+ C: "#38BDF8", // Cyan visor
190
+ E: "#06B6D4", // Visor eye glow
191
+ K: "#0F172A", // Dark visor frame
192
+ M: "#F43F5E", // Rose nose/mouth
193
+ G: "#10B981", // Emerald badge
194
+ B: "#334155", // Slate base
195
+ ".": "transparent",
196
+ };
197
+
198
+ const MASCOT_GRID = [
199
+ "..PP............PP......",
200
+ ".PPPP..........PPPP.....",
201
+ ".PPWW..........WWPP.....",
202
+ "WWWWWWWWWWWWWWWWWWWW....",
203
+ "WWKKKKKKKKKKKKKKWWWW....",
204
+ "WWKCCCCCCCCCCCCKWWWW....",
205
+ "WWKCEKEEEEEEKECKWWWW....",
206
+ "WWKKKKKKKKKKKKKKWWWW....",
207
+ "WWWWWWWWMMWWWWWWWWWW....",
208
+ ".WWWWWWWWWWWWWWWWWW..P..",
209
+ "..WWWW..GGGG..WWWWWWWW..",
210
+ "..BBBB..BBBB..BBBBWWWW..",
211
+ ];
212
+
213
+ export function mascotLines() {
214
+ const lines = [];
215
+ for (let y = 0; y < MASCOT_GRID.length; y += 2) {
216
+ let line = "";
217
+ for (let x = 0; x < MASCOT_GRID[y].length; x++) {
218
+ const top = MASCOT_GRID[y][x];
219
+ const bot = MASCOT_GRID[y + 1][x];
220
+ if (top === "." && bot === ".") line += " ";
221
+ else if (top === "." || bot === ".") {
222
+ const pixel = top === "." ? bot : top;
223
+ line += chalk.hex(MASCOT_COLORS[pixel])(top === "." ? "▄" : "▀");
224
+ } else if (top === bot) {
225
+ line += chalk.hex(MASCOT_COLORS[top])("█");
226
+ } else {
227
+ line += chalk.hex(MASCOT_COLORS[top]).bgHex(MASCOT_COLORS[bot])("▀");
228
+ }
229
+ }
230
+ lines.push(line);
231
+ }
232
+ return lines;
233
+ }
234
+
139
235
  /**
140
- * Banner DevTools sắc nét, hiện đại
236
+ * Banner: Cyber Cat Mascot + Wordmark + Badges
141
237
  */
142
- export function printBanner(version = "1.1.0") {
143
- const ascii = [
144
- " ____ _ _ ____ _ _____ _ ",
145
- " / ___(_) |_ | _ \\ _ _ ___| |__ |_ _(_)_ __ ___ ___ ",
146
- " | | _| | __| | |_) | | | / __| '_ \\ | | | | '_ ` _ \\ / _ \\ ",
147
- " | |_| | | |_ | __/| |_| \\__ \\ | | | | | | | | | | | | | __/ ",
148
- " \\____|_|\\__| |_| \\__,_|___/_| |_| |_| |_|_| |_| |_|\\___| ",
149
- ];
150
- const terminalWidth = process.stdout.columns || 72;
151
- const colors = ["#818CF8", "#38BDF8", "#34D399", "#A7F3D0"];
152
- console.log("");
153
- if (terminalWidth >= 72) ascii.forEach((line) => console.log(multiGradient(line, colors)));
154
- else console.log(multiGradient(" GIT PUSH TIME", colors));
238
+ export function printBanner(version = "1.1.1") {
239
+ const W = getW();
240
+ const inner = Math.max(20, W - 2 - 4);
155
241
  const badges = [
156
242
  chalk.bgHex("#1E1B4B").hex("#818CF8").bold(` v${version} `),
157
243
  chalk.bgHex("#064E3B").hex("#34D399").bold(" SMART GIT "),
158
- chalk.bgHex("#4338CA").hex("#C7D2FE").bold(" GITHUB SYNC "),
159
- chalk.bgHex("#0F172A").hex("#38BDF8").bold(" SECRET SHIELD "),
160
- chalk.bgHex("#78350F").hex("#FDE047").bold(" BACKDATE "),
244
+ chalk.bgHex("#0C4A6E").hex("#38BDF8").bold(" GITHUB SYNC "),
245
+ chalk.bgHex("#4C0519").hex("#FB7185").bold(" SECRET SHIELD "),
246
+ chalk.bgHex("#451A03").hex("#FBBF24").bold(" BACKDATE "),
161
247
  ];
162
- let row = " ";
248
+ const rows = [];
249
+ let row = "";
163
250
  for (const badge of badges) {
164
- const gap = row.trim() ? " " : "";
165
- if (stringWidth(row + gap + badge) > terminalWidth - 2) {
166
- console.log(row);
167
- row = " " + badge;
168
- } else row += gap + badge;
251
+ const next = row ? `${row} ${badge}` : badge;
252
+ if (row && stringWidth(next) > inner) {
253
+ rows.push(row);
254
+ row = badge;
255
+ } else row = next;
169
256
  }
170
- if (row.trim()) console.log(row);
171
- console.log("");
257
+ if (row) rows.push(row);
258
+
259
+ const title = multiGradient("G I T P U S H T I M E", [
260
+ "#818CF8",
261
+ "#38BDF8",
262
+ "#2DD4BF",
263
+ "#F472B6",
264
+ ]);
265
+ const subtitle = chalk.hex("#94A3B8")(
266
+ "Git & GitHub CLI · Đẩy code siêu tốc · An toàn · Đổi giờ commit",
267
+ );
268
+ const mascot = mascotLines();
269
+ const body = [...mascot, "", title, subtitle, "", ...rows].join("\n");
270
+ const frame = boxen(body, {
271
+ borderStyle: "round",
272
+ borderColor: "#6366F1",
273
+ padding: { top: 1, bottom: 1, left: 2, right: 2 },
274
+ width: W,
275
+ textAlignment: "center",
276
+ });
277
+ console.log(`\n${frame}\n`);
172
278
  }
173
279
 
174
280
  /**
175
- * Hiển thị Dashboard trạng thái ngắn gọn ở đầu menu
281
+ * Hiển thị Dashboard trạng thái ngắn gọn & trực quan ở đầu menu
176
282
  */
177
- export function printDashboardStatus({ targetDir = process.cwd(), currentBranch = "main", githubUser = null }) {
178
- const border = chalk.hex("#1E293B");
283
+ export function printDashboardStatus({
284
+ targetDir = process.cwd(),
285
+ currentBranch = "main",
286
+ githubUser = null,
287
+ remoteUrl = null,
288
+ changes = 0,
289
+ lastCommit = null,
290
+ version = "1.1.1",
291
+ }) {
292
+ const border = chalk.hex("#6366F1");
293
+ const W = getW();
294
+ const remote = remoteUrl ? formatRemoteLabel(remoteUrl) : chalk.hex("#94A3B8")("Chưa liên kết");
295
+
179
296
  const ghStatus = githubUser
180
- ? chalk.hex("#34D399").bold(`@${githubUser.login}`) + chalk.hex("#64748B")(" (Đã kết nối)")
181
- : chalk.hex("#94A3B8")("Chưa kết nối") + chalk.hex("#64748B")(" [chọn mục GITHUB để login]");
182
- console.log(boxTop(chalk.hex("#475569")(" [ TRẠNG THÁI ] "), border));
183
- console.log(boxKvDot("Thư mục", chalk.hex("#F8FAFC")(truncateVisual(targetDir, 32)), 14, border));
184
- console.log(boxKvDot("Nhánh (Branch)", chalk.hex("#38BDF8").bold(currentBranch), 14, border));
185
- console.log(boxKvDot("GitHub Account", ghStatus, 14, border));
297
+ ? `${chalk.hex("#38BDF8").bold(`@${githubUser.login}`)} ${chalk.bgHex("#064E3B").hex("#34D399").bold(" [✓ ĐÃ KẾT NỐI] ")}`
298
+ : chalk.bgHex("#1E1B4B").hex("#A5B4FC").bold(" [○ CHƯA ĐĂNG NHẬP] ");
299
+
300
+ const branchPill = changes === 0
301
+ ? chalk.bgHex("#064E3B").hex("#34D399").bold(" [✓ SẠCH] ")
302
+ : chalk.bgHex("#78350F").hex("#FDE047").bold(` [● ${changes} THAY ĐỔI] `);
303
+
304
+ const branchDisplay = `${chalk.hex("#38BDF8").bold(currentBranch)} ${branchPill}`;
305
+
306
+ console.log("");
307
+ console.log(
308
+ boxTop(
309
+ `${chalk.bold.hex("#F8FAFC")(" BẢNG ĐIỀU KHIỂN GIT ")} ${chalk.hex("#94A3B8")(`v${version}`)}`,
310
+ border,
311
+ ),
312
+ );
313
+ console.log(boxRow("", border));
314
+ console.log(boxKvDot("Thư mục", chalk.hex("#F1F5F9").bold(targetDir), 14, border));
315
+ console.log(boxKvDot("Nhánh Git", branchDisplay, 14, border));
316
+ console.log(boxKvDot("GitHub", ghStatus, 14, border));
317
+ console.log(boxKvDot("Remote", remote, 14, border));
318
+
319
+ if (lastCommit && lastCommit.hash) {
320
+ const commitText = `${chalk.hex("#A78BFA").bold(lastCommit.hash)} ${chalk.hex("#CBD5E1")(truncateVisual(lastCommit.message || "", 26))} ${chalk.hex("#64748B")(`(${lastCommit.relativeTime || ""})`)}`;
321
+ console.log(boxKvDot("Commit gần", commitText, 14, border));
322
+ }
323
+
324
+ console.log(boxRow("", border));
186
325
  console.log(boxBottom(border));
187
326
  }
188
327
 
328
+ function formatRemoteLabel(url) {
329
+ const value = String(url || "").trim().replace(/\.git$/i, "");
330
+ const match = value.match(/(?:github\.com[:/])([^/]+\/[^/]+)$/i);
331
+ return match ? chalk.hex("#38BDF8")(match[1]) : chalk.hex("#CBD5E1")(value);
332
+ }
333
+
334
+ export function printPushProgress({ current, total = 6, title, completed = [] }) {
335
+ if (process.stdout.isTTY) process.stdout.write("\u001b[2J\u001b[H");
336
+ const nodes = Array.from({ length: total }, (_, index) => {
337
+ const n = index + 1;
338
+ if (n < current) return chalk.hex("#34D399")("●");
339
+ if (n === current) return chalk.hex("#38BDF8").bold("●");
340
+ return chalk.hex("#475569")("○");
341
+ });
342
+ const rail = nodes.join(chalk.hex("#334155")("──"));
343
+ const lead = `${chalk.hex("#38BDF8").bold("PUSH PROGRESS")} ${rail}`;
344
+ const detail = chalk.hex("#94A3B8")(`[Bước ${current}/${total} · ${title}]`);
345
+ console.log("");
346
+ if (stringWidth(`${lead} ${detail}`) <= getW() - 2) console.log(`${lead} ${detail}`);
347
+ else {
348
+ console.log(lead);
349
+ console.log(` ${detail}`);
350
+ }
351
+ if (completed.length) {
352
+ console.log("");
353
+ for (const item of completed) {
354
+ console.log(
355
+ ` ${chalk.hex("#34D399")("✓")} ${chalk.hex("#94A3B8")(item.label.padEnd(16))} ${chalk.hex("#334155")("·····")} ${chalk.hex("#F1F5F9")(truncateVisual(item.value, getW() - 28))}`,
356
+ );
357
+ }
358
+ }
359
+ }
360
+
189
361
  /**
190
362
  * Tiêu đề phân đoạn với điểm nhấn màu sắc
191
363
  */
192
364
  export function printSection(title) {
193
365
  const W = getW();
194
- const marker = chalk.hex("#38BDF8").bold("»");
195
- const prefix = chalk.hex("#334155")("── ") + marker + chalk.bold.hex("#F8FAFC")(` ${String(title || "").trim()} `);
196
- const remaining = Math.max(0, W - stringWidth(prefix));
366
+ const marker = chalk.hex("#A78BFA").bold("■");
367
+ const maxTitle = Math.max(4, W - 12);
368
+ const fittedTitle = fitStyled(String(title || "").trim(), maxTitle);
369
+ const coreWidth = stringWidth(fittedTitle) + 4;
370
+ const remaining = Math.max(4, W - coreWidth);
371
+ const leftLen = Math.ceil(remaining / 2);
372
+ const rightLen = Math.floor(remaining / 2);
197
373
  console.log("");
198
- console.log(prefix + chalk.hex("#334155")("─".repeat(remaining)));
374
+ console.log(
375
+ chalk.hex("#334155")("─".repeat(leftLen) + " ") +
376
+ marker +
377
+ chalk.bold.hex("#E2E8F0")(` ${fittedTitle} `) +
378
+ chalk.hex("#334155")("─".repeat(rightLen)),
379
+ );
199
380
  }
200
381
 
201
382
  /**
@@ -210,17 +391,22 @@ export function stepLabel(current, total, text) {
210
391
  * Bảng tóm tắt thông tin trước khi Push
211
392
  */
212
393
  export function printSummary({ repoUrl, commitMsg, dateDisplay, branch, force, sourceDir }) {
213
- const border = chalk.hex("#475569");
214
- const title = chalk.bold.bgHex("#4338CA").hex("#FFFFFF")(" XÁC NHẬN THÔNG TIN ĐẨY DỰ ÁN ");
394
+ const border = chalk.hex("#38BDF8");
395
+ const title = chalk.bold.hex("#F8FAFC")(" XÁC NHẬN THÔNG TIN PUSH ");
215
396
  console.log("");
216
397
  console.log(boxTop(title, border));
217
- if (sourceDir) console.log(boxKvDot("Thư mục nguồn", chalk.hex("#34D399")(sourceDir), 16, border));
218
- console.log(boxKvDot("Git Repository", chalk.hex("#38BDF8").underline(repoUrl), 16, border));
219
- console.log(boxKvDot("Commit Message", chalk.hex("#FBBF24").bold(commitMsg), 16, border));
220
- console.log(boxKvDot("Thời gian", chalk.hex("#C084FC")(dateDisplay), 16, border));
221
- console.log(boxKvDot("Nhánh đẩy lên", chalk.bgHex("#065F46").hex("#6EE7B7").bold(` ${branch} `), 16, border));
222
- const forceText = force ? chalk.bgHex("#991B1B").hex("#FCA5A5").bold(" BẬT (--force) [!] ") : chalk.hex("#64748B")("Tắt (chế độ an toàn)");
223
- console.log(boxKvDot("Ghi đè (Force)", forceText, 16, border));
398
+ console.log(boxRow("", border));
399
+ if (sourceDir) console.log(boxKvDot("Thư mục nguồn", chalk.hex("#F1F5F9")(sourceDir), 16, border));
400
+ console.log(boxKvDot("Kho lưu trữ", formatRemoteLabel(repoUrl), 16, border));
401
+ console.log(boxKvDot("Nhánh đích", chalk.hex("#38BDF8").bold(branch), 16, border));
402
+ console.log(boxKvDot("Thông điệp", chalk.hex("#FDE047")(`"${commitMsg}"`), 16, border));
403
+ console.log(boxKvDot("Thời điểm", chalk.hex("#A78BFA")(dateDisplay), 16, border));
404
+ console.log(boxKvDot("Quét Secret", chalk.hex("#34D399")("✓ Tự động kiểm tra trước khi đẩy"), 16, border));
405
+ if (force) {
406
+ console.log(boxRow("", border));
407
+ console.log(boxRow(chalk.hex("#F43F5E").bold(" [!] CẢNH BÁO: Chế độ ghi đè (--force) đang kích hoạt!"), border));
408
+ }
409
+ console.log(boxRow("", border));
224
410
  console.log(boxBottom(border));
225
411
  }
226
412
 
@@ -394,6 +580,11 @@ export function printPrePushSecretWarning({ riskyFiles = [], findings = [] }) {
394
580
  );
395
581
  console.log(boxRow("", border));
396
582
 
583
+ const W2 = getW();
584
+ const maxFileW = Math.max(10, W2 - 14);
585
+ const maxLocW = Math.max(10, W2 - 24);
586
+ const maxPrevW = Math.max(10, W2 - 18);
587
+
397
588
  if (riskyFiles.length > 0) {
398
589
  console.log(
399
590
  boxRow(chalk.bold.hex("#F8FAFC")("File cấu hình nguy cơ:"), border),
@@ -402,7 +593,7 @@ export function printPrePushSecretWarning({ riskyFiles = [], findings = [] }) {
402
593
  showFiles.forEach((f) => {
403
594
  const tree = chalk.hex("#64748B")(" +- ");
404
595
  console.log(
405
- boxRow(tree + chalk.hex("#FCD34D")(truncateVisual(f, 46)), border),
596
+ boxRow(tree + chalk.hex("#FCD34D")(truncateVisual(f, maxFileW)), border),
406
597
  );
407
598
  });
408
599
  if (riskyFiles.length > 4) {
@@ -427,7 +618,7 @@ export function printPrePushSecretWarning({ riskyFiles = [], findings = [] }) {
427
618
  showFindings.forEach((f, idx) => {
428
619
  const isLast = idx === showFindings.length - 1;
429
620
  const tree = chalk.hex("#64748B")(" +- ");
430
- const loc = truncateVisual(`${f.file}:${f.line}`, 26);
621
+ const loc = truncateVisual(`${f.file}:${f.line}`, maxLocW);
431
622
  console.log(
432
623
  boxRow(
433
624
  tree +
@@ -440,7 +631,7 @@ export function printPrePushSecretWarning({ riskyFiles = [], findings = [] }) {
440
631
  const indent = chalk.hex("#64748B")(isLast ? " " : " | ");
441
632
  console.log(
442
633
  boxRow(
443
- indent + chalk.hex("#64748B")(`"${truncateVisual(f.preview, 40)}"`),
634
+ indent + chalk.hex("#64748B")(`"${truncateVisual(f.preview, maxPrevW)}"`),
444
635
  border,
445
636
  ),
446
637
  );
@@ -555,37 +746,72 @@ export function printSecretBlockedBox(rawMessage, details = {}) {
555
746
  /**
556
747
  * Card Thông báo Thành công sau khi Push
557
748
  */
558
- export function printSuccessBox({ branch, repoUrl }) {
559
- const border = chalk.hex("#059669");
560
- const title = chalk.bold.bgHex("#10B981").hex("#064E3B")(" ĐẨY DỰ ÁN LÊN GIT THÀNH CÔNG! ");
749
+ /**
750
+ * Card Thông báo Thành công sau khi Push
751
+ */
752
+ export function printSuccessBox({
753
+ branch,
754
+ repoUrl,
755
+ durationSeconds = null,
756
+ secretStatus = "Đã quét (0 rủi ro)",
757
+ }) {
758
+ const border = chalk.hex("#10B981");
759
+ const title = chalk.bold.hex("#F8FAFC")(" PUSH THÀNH CÔNG ");
561
760
  console.log("");
562
761
  console.log(boxTop(title, border));
563
762
  console.log(boxRow("", border));
564
- const branchPill = chalk.bgHex("#065F46").hex("#6EE7B7").bold(` ${branch} `);
565
- console.log(boxKvDot("Branch", branchPill, 12, border));
566
- console.log(boxKvDot("Remote", chalk.hex("#38BDF8").underline(repoUrl), 12, border));
567
- console.log(boxKvDot("Trạng thái", chalk.hex("#A7F3D0")("Đồng bộ hoàn tất 100%"), 12, border));
763
+ console.log(boxKvDot("Nhánh đích", chalk.hex("#34D399").bold(branch), 18, border));
764
+ console.log(boxKvDot("Trạng thái Secret", chalk.hex("#38BDF8")(secretStatus), 18, border));
765
+ if (durationSeconds !== null) {
766
+ console.log(
767
+ boxKvDot(
768
+ "Thời gian xử lý",
769
+ chalk.hex("#FBBF24").bold(`${durationSeconds.toFixed(1)}s`),
770
+ 18,
771
+ border,
772
+ ),
773
+ );
774
+ }
775
+ const webUrl = toGitHubWebUrl(repoUrl);
776
+ if (webUrl) {
777
+ console.log(boxRow("", border));
778
+ console.log(boxDivider(border));
779
+ console.log(
780
+ boxRow(
781
+ `${chalk.hex("#34D399")("» Xem trên GitHub:")} ${chalk.hex("#38BDF8").underline(webUrl)}`,
782
+ border,
783
+ ),
784
+ );
785
+ } else {
786
+ console.log(boxKvDot("Remote", chalk.hex("#CBD5E1")(truncateVisual(repoUrl, getW() - 14)), 18, border));
787
+ }
568
788
  console.log(boxRow("", border));
569
- console.log(boxRow(chalk.hex("#64748B")("» Xem lịch sử commit: git log -1 --stat"), border));
570
789
  console.log(boxBottom(border));
571
790
  console.log("");
572
791
  }
573
792
 
793
+ function toGitHubWebUrl(url) {
794
+ const match = String(url || "").trim().match(/(?:https?:\/\/github\.com\/|git@github\.com:|ssh:\/\/git@github\.com\/)([^/]+\/[^/]+?)(?:\.git)?$/i);
795
+ return match ? `https://github.com/${match[1]}` : null;
796
+ }
797
+
574
798
  export function printCancelled() {
575
- const border = chalk.hex("#D97706");
576
- const title = chalk.bold.bgHex("#D97706").hex("#FFFFFF")(
799
+ const border = chalk.hex("#64748B");
800
+ const title = chalk.bold.hex("#F8FAFC")(
577
801
  " ĐÃ HỦY THAO TÁC ",
578
802
  );
579
803
  console.log("");
580
804
  console.log(boxTop(title, border));
805
+ console.log(boxRow("", border));
581
806
  console.log(
582
807
  boxRow(
583
- chalk.hex("#FDE68A")(
584
- "Không có bất kỳ thay đổi nào được thực hiện trên repo.",
808
+ chalk.hex("#94A3B8")(
809
+ "Thao tác đã dừng lại an toàn. Không có thay đổi nào được thực hiện.",
585
810
  ),
586
811
  border,
587
812
  ),
588
813
  );
814
+ console.log(boxRow("", border));
589
815
  console.log(boxBottom(border));
590
816
  console.log("");
591
817
  }
@@ -735,51 +961,84 @@ export function printFixResult({
735
961
  failed = [],
736
962
  } = {}) {
737
963
  const W = getW();
964
+ const border = chalk.hex("#10B981");
965
+ const title = chalk.bold.hex("#F8FAFC")(" KẾT QUẢ BẢO VỆ SECRET ");
966
+
738
967
  console.log("");
968
+ console.log(boxTop(title, border));
969
+ console.log(boxRow("", border));
970
+
739
971
  if (fixed.length > 0) {
740
972
  console.log(
741
- chalk.hex("#34D399")(
742
- ` [OK] Đã gỡ ${fixed.length} file khỏi commit: ${truncateVisual(fixed.join(", "), W - 32)}`,
973
+ boxRow(
974
+ `${chalk.hex("#34D399").bold("[OK] Đã gỡ khỏi commit:")} ${chalk.hex("#F1F5F9")(truncateVisual(fixed.join(", "), W - 28))}`,
975
+ border,
743
976
  ),
744
977
  );
745
978
  }
746
979
  if (gitignoreAdded.length > 0) {
747
980
  console.log(
748
- chalk.cyan(
749
- ` [+] Thêm vào .gitignore: ${truncateVisual(gitignoreAdded.join(", "), W - 28)}`,
981
+ boxRow(
982
+ `${chalk.hex("#38BDF8").bold("[+] Thêm vào .gitignore:")} ${chalk.hex("#CBD5E1")(truncateVisual(gitignoreAdded.join(", "), W - 30))}`,
983
+ border,
750
984
  ),
751
985
  );
752
986
  }
753
987
  if (codeFilesWithSecrets && codeFilesWithSecrets.length > 0) {
988
+ console.log(boxRow("", border));
754
989
  console.log(
755
- chalk.yellowBright(
756
- " [!] File code chứa secret (đã gỡ khỏi commit, KHÔNG cho vào .gitignore):",
990
+ boxRow(
991
+ chalk.hex("#FBBF24").bold("[!] File code chứa secret (đã gỡ stage, KHÔNG ignore):"),
992
+ border,
757
993
  ),
758
994
  );
759
995
  codeFilesWithSecrets.forEach((f) =>
760
- console.log(chalk.yellow(` - ${f}`)),
996
+ console.log(boxRow(` - ${chalk.hex("#CBD5E1")(truncateVisual(f, W - 10))}`, border)),
761
997
  );
762
998
  console.log(
763
- chalk.gray(" » Hãy xóa key trong file và thay bằng process.env."),
999
+ boxRow(chalk.hex("#94A3B8")(" » Hãy xóa key trong file và thay bằng process.env."), border),
764
1000
  );
765
1001
  }
766
1002
  if (failed.length > 0) {
767
- failed.forEach((f) => console.log(chalk.red(` [x] ${f.file}: ${f.reason}`)));
1003
+ console.log(boxRow("", border));
1004
+ failed.forEach((f) =>
1005
+ console.log(boxRow(chalk.hex("#F43F5E")(` [x] ${f.file}: ${f.reason}`), border)),
1006
+ );
768
1007
  }
1008
+
1009
+ console.log(boxRow("", border));
1010
+ console.log(boxBottom(border));
1011
+ console.log("");
769
1012
  }
770
1013
 
771
1014
  export function printFooter() {
772
- console.log(chalk.hex("#334155")(" ─── » ───"));
773
- console.log(
774
- chalk.hex("#64748B")(" » Mẹo: Chạy ") +
775
- chalk.hex("#38BDF8").bold("git-push-time --help") +
776
- chalk.hex("#64748B")(" để xem toàn bộ danh sách lệnh và phím tắt."),
777
- );
1015
+ const W = getW();
1016
+ // Dải phân cách căn đều 2 bên, full width như mọi khung.
1017
+ const core = "»";
1018
+ const remaining = Math.max(4, W - 3); // 1 core + 2 space
1019
+ const leftLen = Math.ceil(remaining / 2);
1020
+ const rightLen = Math.floor(remaining / 2);
778
1021
  console.log(
779
- chalk.hex("#475569")(
780
- " » GitHub CLI & Push Time — Chúc bạn làm việc hiệu quả!\n",
781
- ),
1022
+ chalk.hex("#334155")("─".repeat(leftLen) + " ") +
1023
+ chalk.hex("#64748B")(core) +
1024
+ chalk.hex("#334155")(" " + "─".repeat(rightLen))
782
1025
  );
1026
+ const tip1 =
1027
+ chalk.hex("#64748B")("» Mẹo: Chạy ") +
1028
+ chalk.hex("#38BDF8").bold("git-push-time --help") +
1029
+ chalk.hex("#64748B")(" để xem toàn bộ lệnh.");
1030
+ const tip2 = chalk.hex("#475569")("» GitHub CLI & Push Time — Chúc hiệu quả!");
1031
+ for (const tip of [tip1, tip2]) {
1032
+ const w = stringWidth(tip);
1033
+ if (w >= W) {
1034
+ console.log(` ${tip}`);
1035
+ } else {
1036
+ const padTotal = W - w;
1037
+ const left = Math.floor(padTotal / 2);
1038
+ console.log(" ".repeat(left) + tip);
1039
+ }
1040
+ }
1041
+ console.log("");
783
1042
  }
784
1043
 
785
1044
  export function styleCliHelp(program) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "git-push-time",
3
- "version": "1.1.1",
3
+ "version": "1.1.2",
4
4
  "description": "CLI tool to push project to Git with custom repo URL and custom commit timestamp",
5
5
  "main": "bin/index.js",
6
6
  "type": "module",
@@ -26,6 +26,7 @@
26
26
  "license": "MIT",
27
27
  "dependencies": {
28
28
  "@inquirer/prompts": "^7.3.2",
29
+ "boxen": "^9.0.0",
29
30
  "chalk": "^5.4.1",
30
31
  "commander": "^13.1.0",
31
32
  "dayjs": "^1.11.13",