git-push-time 1.1.2 → 1.1.4

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.
@@ -30,7 +30,7 @@ function printOpenHeader(title, hintLines = []) {
30
30
  function printCloseWithAnswer(label, value) {
31
31
  const W = getW();
32
32
  const answer = value === BACK || value === undefined
33
- ? chalk.hex('#FCD34D')('« Quay lại')
33
+ ? chalk.hex('#FCD34D')('« Go back')
34
34
  : chalk.hex('#34D399').bold(truncateVisual(String(value ?? ''), Math.max(10, W - 14)));
35
35
  console.log(
36
36
  boxRow(
@@ -49,7 +49,7 @@ function shortChoiceName(choices, value) {
49
49
  try {
50
50
  const found = (choices || []).find((c) => c && c.value === value);
51
51
  if (!found) return String(value ?? '');
52
- // name có thể chứa màu chalk -> strip bằng cách lấy raw rồi cắt
52
+ // The name may contain Chalk colors; strip them before displaying it.
53
53
  return String(found.short || found.name || found.value || '').replace(/\u001b\[[0-9;]*m/g, '');
54
54
  } catch {
55
55
  return String(value ?? '');
@@ -82,13 +82,12 @@ const baseSelectTheme = {
82
82
  };
83
83
 
84
84
  function promptLine(label) {
85
- // Dòng nhập liệu mang viền trái │ để thị giác nằm gọn trong khung
86
- // đang mở phía trên (boxTop) và sẽ đóng (boxBottom) sau khi xong.
85
+ // Keep the input line inside the frame opened above and closed after completion.
87
86
  return `${BORDER('│')} ${ACCENT('❯')} ${chalk.whiteBright(cleanTitle(label))}`;
88
87
  }
89
88
 
90
89
  /**
91
- * Select nằm TRONG khung: header mở -> choices -> footer đóng + đáp án.
90
+ * Render a select inside a frame: open header, choices, then close with the answer.
92
91
  */
93
92
  export async function selectBox({ message, choices, hint, pageSize, ...rest }) {
94
93
  const { clearPromptOnDone: _ignore, theme: customTheme, ...cfg } = rest;
@@ -99,7 +98,7 @@ export async function selectBox({ message, choices, hint, pageSize, ...rest }) {
99
98
  try {
100
99
  value = await select(
101
100
  {
102
- message: promptLine('Chọn'),
101
+ message: promptLine('Select'),
103
102
  choices,
104
103
  pageSize: pageSize || Math.min(12, (choices || []).length + 2),
105
104
  theme: { ...baseSelectTheme, ...(customTheme || {}) },
@@ -116,7 +115,7 @@ export async function selectBox({ message, choices, hint, pageSize, ...rest }) {
116
115
  }
117
116
 
118
117
  /**
119
- * Prompt select có sẵn lựa chọn « Quay lại (nằm trong khung).
118
+ * Select prompt with a built-in Back option.
120
119
  */
121
120
  export async function selectWithBack({ message, choices, hint, pageSize, ...rest }) {
122
121
  const hasBack = (choices || []).some((c) => c && c.value === BACK);
@@ -124,20 +123,20 @@ export async function selectWithBack({ message, choices, hint, pageSize, ...rest
124
123
  ? choices
125
124
  : [
126
125
  ...choices,
127
- { name: '« Quay lại (bước trước)', value: BACK, description: 'Về bước trước / menu chính' }
126
+ { name: '« Go back (previous step)', value: BACK, description: 'Return to the previous step or main menu' }
128
127
  ];
129
- const backHint = hint ?? '↑↓ di chuyển · Enter chọn · chọn « Quay lại để lùi bước';
128
+ const backHint = hint ?? '↑↓ move · Enter select · choose « Go back to return to the previous step';
130
129
  return await selectBox({ message, choices: finalChoices, hint: backHint, pageSize, ...rest });
131
130
  }
132
131
 
133
132
  /**
134
- * Prompt input nằm TRONG khung: header mở -> ô gõ (│ › ...) -> footer + đáp án.
133
+ * Render an input prompt inside a frame with a header and answer footer.
135
134
  */
136
- export async function inputBox({ message, promptLabel = 'Nhập', hint, default: def, validate, ...rest }) {
135
+ export async function inputBox({ message, promptLabel = 'Enter', hint, default: def, validate, ...rest }) {
137
136
  const { clearPromptOnDone: _ignore, theme: customTheme, ...cfg } = rest;
138
137
  const hintLines = [];
139
138
  if (hint) hintLines.push(...(Array.isArray(hint) ? hint : [hint]));
140
- if (def) hintLines.push(`Mặc định: ${def}`);
139
+ if (def) hintLines.push(`Default: ${def}`);
141
140
  printOpenHeader(message, hintLines);
142
141
 
143
142
  const wrappedValidate = (val) => {
@@ -171,24 +170,24 @@ export async function inputBox({ message, promptLabel = 'Nhập', hint, default:
171
170
  }
172
171
 
173
172
  /**
174
- * Prompt input có hướng dẫn gõ ".." để quay lại (nằm trong khung).
173
+ * Input prompt with instructions to type ".." to go back.
175
174
  */
176
175
  export async function inputWithBack({ message, hint, validate, ...rest }) {
177
- const backHint = hint ?? 'Gõ ".." để quay lại bước trước';
176
+ const backHint = hint ?? 'Type ".." to return to the previous step';
178
177
  return await inputBox({ message, hint: backHint, validate, ...rest });
179
178
  }
180
179
 
181
180
  /**
182
- * Hỏi Tiếp tục / Quay lại menu / Thoát (nằm trong khung).
181
+ * Ask whether to continue, return to the menu, or exit.
183
182
  */
184
- export async function promptContinueOrBack(message = 'Bạn muốn tiếp tục hay quay lại?') {
183
+ export async function promptContinueOrBack(message = 'Would you like to continue or go back?') {
185
184
  return await selectBox({
186
185
  message,
187
- hint: 'Chọn bước tiếp theo',
186
+ hint: 'Choose what to do next',
188
187
  choices: [
189
- { name: 'Tiếp tục', value: 'continue', description: 'Đi tiếp bước kế tiếp' },
190
- { name: 'Quay lại bảng điều khiển', value: 'back', description: 'Về màn hình chọn hành động' },
191
- { name: 'Thoát', value: 'exit', description: 'Dừng, không làm gì' }
188
+ { name: 'Continue', value: 'continue', description: 'Proceed to the next step' },
189
+ { name: 'Back to dashboard', value: 'back', description: 'Return to the action menu' },
190
+ { name: 'Exit', value: 'exit', description: 'Stop without continuing' }
192
191
  ],
193
192
  });
194
193
  }
package/lib/secrets.js CHANGED
@@ -1,92 +1,92 @@
1
- import { spawnSync } from 'child_process';
2
-
3
- // Pattern quét secret phổ biến. Giữ ở mức precision cao để tránh false-positive.
4
- export const SECRET_PATTERNS = [
5
- { id: 'npm-token', label: 'npm Access Token', regex: /npm_[A-Za-z0-9]{8,}/ },
6
- { id: 'npm-authtoken', label: '_authToken trong .npmrc', regex: /_authToken\s*=\s*\S+/i },
7
- { id: 'github-token', label: 'GitHub Token (ghp_/gho_/github_pat_)', regex: /\b(ghp_[A-Za-z0-9]{20,}|gho_[A-Za-z0-9]{20,}|github_pat_[A-Za-z0-9_]{40,})\b/ },
8
- { id: 'aws-key', label: 'AWS Access Key (AKIA...)', regex: /\bAKIA[0-9A-Z]{16}\b/ },
9
- { id: 'aws-secret', label: 'AWS Secret Key', regex: /aws_secret_access_key\s*=\s*\S+/i },
10
- { id: 'private-key', label: 'Private Key (PEM)', regex: /-----BEGIN (RSA |EC |OPENSSH |DSA )?PRIVATE KEY-----/ },
11
- { id: 'openai-key', label: 'OpenAI API Key', regex: /\bsk-(proj-)?[A-Za-z0-9_-]{20,}\b/ },
12
- { id: 'slack-token', label: 'Slack Token', regex: /\bxox[baprs]-[A-Za-z0-9-]{10,}\b/ },
13
- { id: 'google-key', label: 'Google API Key', regex: /\bAIza[0-9A-Za-z_-]{30,}\b/ },
14
- { id: 'generic-password', label: 'Password gán cứng', regex: /\b(password|passwd|pwd)\s*[:=]\s*['"][^'"]{4,}['"]/i },
15
- ];
16
-
17
- const SKIP_DIRS = ['node_modules/', '.git/', 'dist/', 'build/', '.next/'];
18
- const SKIP_FILES = ['package-lock.json', 'yarn.lock', 'pnpm-lock.yaml'];
19
- const MAX_FILE_BYTES = 512 * 1024; // bỏ qua file staged > 512KB
20
- const MAX_MATCHES_PER_FILE = 3;
21
-
22
- function listStagedFiles(cwd) {
23
- try {
24
- const r = spawnSync('git', ['diff', '--cached', '--name-only', '--diff-filter=ACM'], {
25
- cwd, encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore']
26
- });
27
- if (r.status !== 0) return [];
28
- return (r.stdout || '').split(/\r?\n/).map((s) => s.trim()).filter(Boolean);
29
- } catch {
30
- return [];
31
- }
32
- }
33
-
34
- function readStagedContent(file, cwd) {
35
- try {
36
- // Dùng spawnSync dạng mảng args (không qua shell) để tránh injection
37
- // khi tên file chứa ký tự đặc biệt (", $, ;, &, |, ...).
38
- const r = spawnSync('git', ['show', `:${file}`], {
39
- cwd, encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'], maxBuffer: MAX_FILE_BYTES * 2
40
- });
41
- if (r.status !== 0) return null;
42
- const content = r.stdout || '';
43
- if (content.length > MAX_FILE_BYTES) return null; // file quá lớn, bỏ qua
44
- if (content.includes('\0')) return null; // file binary
45
- return content;
46
- } catch {
47
- return null;
48
- }
49
- }
50
-
51
- function redactPreview(line) {
52
- const s = line.trim().slice(0, 120);
53
- // Che giữa secret, chỉ chừa 4 ký tự đầu/cuối
54
- return s.replace(/([A-Za-z0-9_.-]{4})[A-Za-z0-9_.-]{6,}([A-Za-z0-9_.-]{2})?/g, '$1•••$2');
55
- }
56
-
57
- /**
58
- * Quét nội dung các file đã stage để tìm secret.
59
- * @param {string} cwd
60
- * @returns {{ file: string, type: string, line: number, preview: string }[]}
61
- */
62
- export function scanStagedContentForSecrets(cwd = process.cwd()) {
63
- const findings = [];
64
- const files = listStagedFiles(cwd).filter((f) => {
65
- const normalized = String(f).replace(/\\/g, '/');
66
- if (SKIP_DIRS.some((d) => normalized.startsWith(d) || normalized.includes('/' + d))) return false;
67
- if (SKIP_FILES.includes(normalized.split('/').pop())) return false;
68
- return true;
69
- });
70
-
71
- for (const file of files.slice(0, 100)) { // giới hạn 100 file để nhanh
72
- const content = readStagedContent(file, cwd);
73
- if (!content) continue;
74
- const lines = content.split('\n');
75
- let matchesInFile = 0;
76
- for (let i = 0; i < lines.length; i++) {
77
- const line = lines[i];
78
- if (!line || line.length > 2000) continue;
79
- // Bỏ qua comment ví dụ trong docs? vẫn báo để user tự quyết
80
- for (const p of SECRET_PATTERNS) {
81
- if (p.regex.test(line)) {
82
- findings.push({ file, type: p.label, line: i + 1, preview: redactPreview(line) });
83
- matchesInFile++;
84
- break; // mỗi dòng chỉ báo 1 lần
85
- }
86
- }
87
- if (matchesInFile >= MAX_MATCHES_PER_FILE) break;
88
- if (findings.length >= 20) return findings; // chặn spam
89
- }
90
- }
91
- return findings;
92
- }
1
+ import { spawnSync } from 'child_process';
2
+
3
+ // Common secret patterns, tuned for high precision to reduce false positives.
4
+ export const SECRET_PATTERNS = [
5
+ { id: 'npm-token', label: 'npm Access Token', regex: /npm_[A-Za-z0-9]{8,}/ },
6
+ { id: 'npm-authtoken', label: '_authToken trong .npmrc', regex: /_authToken\s*=\s*\S+/i },
7
+ { id: 'github-token', label: 'GitHub Token (ghp_/gho_/github_pat_)', regex: /\b(ghp_[A-Za-z0-9]{20,}|gho_[A-Za-z0-9]{20,}|github_pat_[A-Za-z0-9_]{40,})\b/ },
8
+ { id: 'aws-key', label: 'AWS Access Key (AKIA...)', regex: /\bAKIA[0-9A-Z]{16}\b/ },
9
+ { id: 'aws-secret', label: 'AWS Secret Key', regex: /aws_secret_access_key\s*=\s*\S+/i },
10
+ { id: 'private-key', label: 'Private Key (PEM)', regex: /-----BEGIN (RSA |EC |OPENSSH |DSA )?PRIVATE KEY-----/ },
11
+ { id: 'openai-key', label: 'OpenAI API Key', regex: /\bsk-(proj-)?[A-Za-z0-9_-]{20,}\b/ },
12
+ { id: 'slack-token', label: 'Slack Token', regex: /\bxox[baprs]-[A-Za-z0-9-]{10,}\b/ },
13
+ { id: 'google-key', label: 'Google API Key', regex: /\bAIza[0-9A-Za-z_-]{30,}\b/ },
14
+ { id: 'generic-password', label: 'Hard-coded password', regex: /\b(password|passwd|pwd)\s*[:=]\s*['"][^'"]{4,}['"]/i },
15
+ ];
16
+
17
+ const SKIP_DIRS = ['node_modules/', '.git/', 'dist/', 'build/', '.next/'];
18
+ const SKIP_FILES = ['package-lock.json', 'yarn.lock', 'pnpm-lock.yaml'];
19
+ const MAX_FILE_BYTES = 512 * 1024; // Skip staged files larger than 512 KB.
20
+ const MAX_MATCHES_PER_FILE = 3;
21
+
22
+ function listStagedFiles(cwd) {
23
+ try {
24
+ const r = spawnSync('git', ['diff', '--cached', '--name-only', '--diff-filter=ACM'], {
25
+ cwd, encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore']
26
+ });
27
+ if (r.status !== 0) return [];
28
+ return (r.stdout || '').split(/\r?\n/).map((s) => s.trim()).filter(Boolean);
29
+ } catch {
30
+ return [];
31
+ }
32
+ }
33
+
34
+ function readStagedContent(file, cwd) {
35
+ try {
36
+ // Pass arguments as an array instead of using a shell to avoid injection
37
+ // when filenames contain special characters (", $, ;, &, |, ...).
38
+ const r = spawnSync('git', ['show', `:${file}`], {
39
+ cwd, encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'], maxBuffer: MAX_FILE_BYTES * 2
40
+ });
41
+ if (r.status !== 0) return null;
42
+ const content = r.stdout || '';
43
+ if (content.length > MAX_FILE_BYTES) return null; // File is too large; skip it.
44
+ if (content.includes('\0')) return null; // file binary
45
+ return content;
46
+ } catch {
47
+ return null;
48
+ }
49
+ }
50
+
51
+ function redactPreview(line) {
52
+ const s = line.trim().slice(0, 120);
53
+ // Mask the secret, leaving only the first four and last two characters.
54
+ return s.replace(/([A-Za-z0-9_.-]{4})[A-Za-z0-9_.-]{6,}([A-Za-z0-9_.-]{2})?/g, '$1•••$2');
55
+ }
56
+
57
+ /**
58
+ * Scan staged file contents for secrets.
59
+ * @param {string} cwd
60
+ * @returns {{ file: string, type: string, line: number, preview: string }[]}
61
+ */
62
+ export function scanStagedContentForSecrets(cwd = process.cwd()) {
63
+ const findings = [];
64
+ const files = listStagedFiles(cwd).filter((f) => {
65
+ const normalized = String(f).replace(/\\/g, '/');
66
+ if (SKIP_DIRS.some((d) => normalized.startsWith(d) || normalized.includes('/' + d))) return false;
67
+ if (SKIP_FILES.includes(normalized.split('/').pop())) return false;
68
+ return true;
69
+ });
70
+
71
+ for (const file of files.slice(0, 100)) { // Limit to 100 files for speed.
72
+ const content = readStagedContent(file, cwd);
73
+ if (!content) continue;
74
+ const lines = content.split('\n');
75
+ let matchesInFile = 0;
76
+ for (let i = 0; i < lines.length; i++) {
77
+ const line = lines[i];
78
+ if (!line || line.length > 2000) continue;
79
+ // Still report matches in documentation examples so the user can decide.
80
+ for (const p of SECRET_PATTERNS) {
81
+ if (p.regex.test(line)) {
82
+ findings.push({ file, type: p.label, line: i + 1, preview: redactPreview(line) });
83
+ matchesInFile++;
84
+ break; // Report at most once per line.
85
+ }
86
+ }
87
+ if (matchesInFile >= MAX_MATCHES_PER_FILE) break;
88
+ if (findings.length >= 20) return findings; // Limit output to 20 findings.
89
+ }
90
+ }
91
+ return findings;
92
+ }