git-push-time 1.1.3 → 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.
package/lib/github.js CHANGED
@@ -5,7 +5,7 @@ const GITHUB_API_BASE = 'https://api.github.com';
5
5
  const GITHUB_OAUTH_CLIENT_ID = '178c6fc778ccc68e1d6a';
6
6
 
7
7
  /**
8
- * Mở URL trên trình duyệt web mặc định của hệ thống
8
+ * Open a URL in the system's default web browser.
9
9
  * @param {string} url
10
10
  */
11
11
  export function openBrowser(url) {
@@ -25,7 +25,7 @@ export function openBrowser(url) {
25
25
  }
26
26
 
27
27
  /**
28
- * Khởi tạo GitHub OAuth Device Authorization Flow
28
+ * Start the GitHub OAuth Device Authorization Flow.
29
29
  * @returns {Promise<{ ok: boolean, data?: { device_code: string, user_code: string, verification_uri: string, expires_in: number, interval: number }, error?: string }>}
30
30
  */
31
31
  export async function initiateDeviceFlow() {
@@ -44,18 +44,18 @@ export async function initiateDeviceFlow() {
44
44
 
45
45
  if (!res.ok) {
46
46
  const err = await res.json().catch(() => ({}));
47
- return { ok: false, error: err.error_description || err.message || `Lỗi API (${res.status})` };
47
+ return { ok: false, error: err.error_description || err.message || `API error (${res.status})` };
48
48
  }
49
49
 
50
50
  const data = await res.json();
51
51
  return { ok: true, data };
52
52
  } catch (err) {
53
- return { ok: false, error: `Lỗi kết nối mạng: ${err.message}` };
53
+ return { ok: false, error: `Network error: ${err.message}` };
54
54
  }
55
55
  }
56
56
 
57
57
  /**
58
- * Polling kiểm tra người dùng đã Authorize trên GitHub chưa
58
+ * Poll until the user authorizes the application on GitHub.
59
59
  * @param {string} deviceCode
60
60
  * @param {number} intervalSeconds
61
61
  * @param {number} expiresInSeconds
@@ -104,29 +104,29 @@ export async function pollDeviceToken(deviceCode, intervalSeconds = 5, expiresIn
104
104
  }
105
105
 
106
106
  if (data.error === 'expired_token') {
107
- return { ok: false, error: 'Mã xác thực đã hết hạn. Vui lòng thử lại.' };
107
+ return { ok: false, error: 'The verification code expired. Please try again.' };
108
108
  }
109
109
 
110
110
  if (data.error === 'access_denied') {
111
- return { ok: false, error: 'Bạn đã từ chối cấp quyền trên GitHub.' };
111
+ return { ok: false, error: 'You denied authorization on GitHub.' };
112
112
  }
113
113
 
114
- return { ok: false, error: data.error_description || data.error || 'Lỗi xác thực không xác định' };
114
+ return { ok: false, error: data.error_description || data.error || 'Unknown authorization error' };
115
115
  } catch (err) {
116
- // Mạng chập chờn: thử lại vài lần, lỗi liên tục thì thoát sớm thay vì chờ hết 15 phút
116
+ // Retry intermittent network errors and exit early after repeated failures.
117
117
  consecutiveNetErrors++;
118
118
  if (consecutiveNetErrors >= 5) {
119
- return { ok: false, error: `Lỗi kết nối mạng lặp lại (${err.message}). Vui lòng kiểm tra mạng rồi thử lại.` };
119
+ return { ok: false, error: `Repeated network error (${err.message}). Check your connection and try again.` };
120
120
  }
121
121
  continue;
122
122
  }
123
123
  }
124
124
 
125
- return { ok: false, error: 'Hết thời gian chờ cấp quyền (Timeout).' };
125
+ return { ok: false, error: 'Authorization timed out.' };
126
126
  }
127
127
 
128
128
  /**
129
- * Tạo headers chuẩn cho GitHub REST API
129
+ * Create standard headers for the GitHub REST API.
130
130
  * @param {string} token
131
131
  * @returns {Record<string, string>}
132
132
  */
@@ -140,13 +140,13 @@ function getApiHeaders(token) {
140
140
  }
141
141
 
142
142
  /**
143
- * Kiểm tra token và lấy thông tin tài khoản GitHub
143
+ * Verify a token and fetch the GitHub account profile.
144
144
  * @param {string} token
145
145
  * @returns {Promise<{ ok: boolean, user?: any, error?: string }>}
146
146
  */
147
147
  export async function verifyAndFetchUser(token) {
148
148
  if (!token || !token.trim()) {
149
- return { ok: false, error: 'Token không được để trống.' };
149
+ return { ok: false, error: 'Token cannot be empty.' };
150
150
  }
151
151
 
152
152
  try {
@@ -156,42 +156,42 @@ export async function verifyAndFetchUser(token) {
156
156
  });
157
157
 
158
158
  if (res.status === 401) {
159
- return { ok: false, error: 'Token không hợp lệ hoặc đã hết hạn (401 Unauthorized).' };
159
+ return { ok: false, error: 'Token is invalid or expired (401 Unauthorized).' };
160
160
  }
161
161
 
162
162
  if (!res.ok) {
163
163
  const errData = await res.json().catch(() => ({}));
164
- return { ok: false, error: errData.message || `Lỗi kết nối GitHub API (${res.status})` };
164
+ return { ok: false, error: errData.message || `GitHub API error (${res.status})` };
165
165
  }
166
166
 
167
167
  const user = await res.json();
168
168
  return { ok: true, user };
169
169
  } catch (err) {
170
- return { ok: false, error: `Lỗi kết nối mạng: ${err.message}` };
170
+ return { ok: false, error: `Network error: ${err.message}` };
171
171
  }
172
172
  }
173
173
 
174
174
  /**
175
- * Lấy thông tin user hiện tại từ token đang kích hoạt
175
+ * Get the current user profile from the active token.
176
176
  * @returns {Promise<{ authenticated: boolean, user?: any, source?: string, token?: string, error?: string }>}
177
177
  */
178
178
  export async function getCurrentGitHubUser() {
179
179
  const { token, source, user: cachedUser } = getGitHubToken();
180
180
  if (!token) {
181
- return { authenticated: false, error: 'Chưa kết nối tài khoản GitHub' };
181
+ return { authenticated: false, error: 'No GitHub account is connected.' };
182
182
  }
183
183
 
184
- // Thử verify với API
184
+ // Verify the token with the API.
185
185
  const check = await verifyAndFetchUser(token);
186
186
  if (!check.ok) {
187
- // Nếu lỗi mạng nhưng có cache user
187
+ // Fall back to the cached profile if the network request fails.
188
188
  if (cachedUser) {
189
189
  return { authenticated: true, user: cachedUser, token, source, offline: true };
190
190
  }
191
191
  return { authenticated: false, error: check.error, source };
192
192
  }
193
193
 
194
- // Cập nhật lại cache user vào config — chỉ ghi khi có thay đổi để tránh I/O mỗi lần render menu
194
+ // Update the cached profile only when it changes to avoid disk I/O on each menu render.
195
195
  if (source === 'config') {
196
196
  const cachedLogin = cachedUser?.login;
197
197
  if (!cachedLogin || cachedLogin !== check.user?.login || JSON.stringify(cachedUser) !== JSON.stringify({
@@ -212,16 +212,16 @@ export async function getCurrentGitHubUser() {
212
212
  }
213
213
 
214
214
  /**
215
- * Lấy danh sách repository của user hiện tại
215
+ * List repositories for the current user.
216
216
  * @param {object} options
217
- * @param {number} options.limit Số lượng repo tối đa
217
+ * @param {number} options.limit Maximum number of repositories to return
218
218
  * @param {'all' | 'owner' | 'public' | 'private'} options.type
219
219
  * @returns {Promise<{ ok: boolean, repos?: any[], error?: string }>}
220
220
  */
221
221
  export async function listUserRepos({ limit = 30, type = 'owner' } = {}) {
222
222
  const { token } = getGitHubToken();
223
223
  if (!token) {
224
- return { ok: false, error: 'Chưa có GitHub Token. Vui lòng kết nối tài khoản trước.' };
224
+ return { ok: false, error: 'No GitHub token found. Connect an account first.' };
225
225
  }
226
226
 
227
227
  try {
@@ -238,38 +238,38 @@ export async function listUserRepos({ limit = 30, type = 'owner' } = {}) {
238
238
 
239
239
  if (!res.ok) {
240
240
  const errData = await res.json().catch(() => ({}));
241
- return { ok: false, error: errData.message || `Không thể tải danh sách repo (${res.status})` };
241
+ return { ok: false, error: errData.message || `Could not load repositories (${res.status})` };
242
242
  }
243
243
 
244
244
  const repos = await res.json();
245
245
  return { ok: true, repos };
246
246
  } catch (err) {
247
- return { ok: false, error: `Lỗi kết nối: ${err.message}` };
247
+ return { ok: false, error: `Connection error: ${err.message}` };
248
248
  }
249
249
  }
250
250
 
251
251
  /**
252
- * Tạo repository mới trên GitHub
252
+ * Create a new GitHub repository.
253
253
  * @param {object} params
254
- * @param {string} params.name Tên repo (bắt buộc)
255
- * @param {string} [params.description] Mô tả repo
256
- * @param {boolean} [params.isPrivate] Repo riêng tư hay công khai
257
- * @param {boolean} [params.autoInit] Có tự tạo README.md ban đầu không (mặc định false để push project có sẵn)
254
+ * @param {string} params.name Repository name (required)
255
+ * @param {string} [params.description] Repository description
256
+ * @param {boolean} [params.isPrivate] Whether the repository is private
257
+ * @param {boolean} [params.autoInit] Whether to create an initial README.md (defaults to false for existing projects)
258
258
  * @returns {Promise<{ ok: boolean, repo?: any, error?: string }>}
259
259
  */
260
260
  export async function createGitHubRepo({ name, description = '', isPrivate = false, autoInit = false }) {
261
261
  const { token } = getGitHubToken();
262
262
  if (!token) {
263
- return { ok: false, error: 'Chưa có GitHub Token. Vui lòng kết nối tài khoản trước.' };
263
+ return { ok: false, error: 'No GitHub token found. Connect an account first.' };
264
264
  }
265
265
 
266
266
  const cleanName = String(name || '').trim();
267
267
  if (!cleanName) {
268
- return { ok: false, error: 'Tên repository không được để trống.' };
268
+ return { ok: false, error: 'Repository name cannot be empty.' };
269
269
  }
270
270
 
271
271
  if (!/^[a-zA-Z0-9_.-]+$/.test(cleanName)) {
272
- return { ok: false, error: 'Tên repo chỉ được chứa chữ cái, số, dấu gạch nối (-), gạch dưới (_), hoặc dấu chấm (.).' };
272
+ return { ok: false, error: 'Repository names may contain only letters, numbers, hyphens (-), underscores (_), and periods (.).' };
273
273
  }
274
274
 
275
275
  try {
@@ -291,13 +291,13 @@ export async function createGitHubRepo({ name, description = '', isPrivate = fal
291
291
 
292
292
  if (res.status === 422) {
293
293
  const errData = await res.json().catch(() => ({}));
294
- const msg = (errData.errors && errData.errors[0]?.message) || errData.message || 'Tên repository đã tồn tại hoặc không hợp lệ.';
295
- return { ok: false, error: `Không thể tạo repo: ${msg}` };
294
+ const msg = (errData.errors && errData.errors[0]?.message) || errData.message || 'Repository already exists or the name is invalid.';
295
+ return { ok: false, error: `Could not create repository: ${msg}` };
296
296
  }
297
297
 
298
298
  if (!res.ok) {
299
299
  const errData = await res.json().catch(() => ({}));
300
- return { ok: false, error: errData.message || `Lỗi từ GitHub API (${res.status})` };
300
+ return { ok: false, error: errData.message || `GitHub API error (${res.status})` };
301
301
  }
302
302
 
303
303
  const repo = await res.json();
@@ -317,6 +317,6 @@ export async function createGitHubRepo({ name, description = '', isPrivate = fal
317
317
  }
318
318
  };
319
319
  } catch (err) {
320
- return { ok: false, error: `Lỗi kết nối khi tạo repo: ${err.message}` };
320
+ return { ok: false, error: `Connection error while creating repository: ${err.message}` };
321
321
  }
322
322
  }
@@ -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,6 +1,6 @@
1
1
  import { spawnSync } from 'child_process';
2
2
 
3
- // Pattern quét secret phổ biến. Giữ ở mức precision cao để tránh false-positive.
3
+ // Common secret patterns, tuned for high precision to reduce false positives.
4
4
  export const SECRET_PATTERNS = [
5
5
  { id: 'npm-token', label: 'npm Access Token', regex: /npm_[A-Za-z0-9]{8,}/ },
6
6
  { id: 'npm-authtoken', label: '_authToken trong .npmrc', regex: /_authToken\s*=\s*\S+/i },
@@ -11,12 +11,12 @@ export const SECRET_PATTERNS = [
11
11
  { id: 'openai-key', label: 'OpenAI API Key', regex: /\bsk-(proj-)?[A-Za-z0-9_-]{20,}\b/ },
12
12
  { id: 'slack-token', label: 'Slack Token', regex: /\bxox[baprs]-[A-Za-z0-9-]{10,}\b/ },
13
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 },
14
+ { id: 'generic-password', label: 'Hard-coded password', regex: /\b(password|passwd|pwd)\s*[:=]\s*['"][^'"]{4,}['"]/i },
15
15
  ];
16
16
 
17
17
  const SKIP_DIRS = ['node_modules/', '.git/', 'dist/', 'build/', '.next/'];
18
18
  const SKIP_FILES = ['package-lock.json', 'yarn.lock', 'pnpm-lock.yaml'];
19
- const MAX_FILE_BYTES = 512 * 1024; // bỏ qua file staged > 512KB
19
+ const MAX_FILE_BYTES = 512 * 1024; // Skip staged files larger than 512 KB.
20
20
  const MAX_MATCHES_PER_FILE = 3;
21
21
 
22
22
  function listStagedFiles(cwd) {
@@ -33,14 +33,14 @@ function listStagedFiles(cwd) {
33
33
 
34
34
  function readStagedContent(file, cwd) {
35
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 (", $, ;, &, |, ...).
36
+ // Pass arguments as an array instead of using a shell to avoid injection
37
+ // when filenames contain special characters (", $, ;, &, |, ...).
38
38
  const r = spawnSync('git', ['show', `:${file}`], {
39
39
  cwd, encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'], maxBuffer: MAX_FILE_BYTES * 2
40
40
  });
41
41
  if (r.status !== 0) return null;
42
42
  const content = r.stdout || '';
43
- if (content.length > MAX_FILE_BYTES) return null; // file quá lớn, bỏ qua
43
+ if (content.length > MAX_FILE_BYTES) return null; // File is too large; skip it.
44
44
  if (content.includes('\0')) return null; // file binary
45
45
  return content;
46
46
  } catch {
@@ -50,12 +50,12 @@ function readStagedContent(file, cwd) {
50
50
 
51
51
  function redactPreview(line) {
52
52
  const s = line.trim().slice(0, 120);
53
- // Che giữa secret, chỉ chừa 4 ký tự đầu/cuối
53
+ // Mask the secret, leaving only the first four and last two characters.
54
54
  return s.replace(/([A-Za-z0-9_.-]{4})[A-Za-z0-9_.-]{6,}([A-Za-z0-9_.-]{2})?/g, '$1•••$2');
55
55
  }
56
56
 
57
57
  /**
58
- * Quét nội dung các file đã stage để tìm secret.
58
+ * Scan staged file contents for secrets.
59
59
  * @param {string} cwd
60
60
  * @returns {{ file: string, type: string, line: number, preview: string }[]}
61
61
  */
@@ -68,7 +68,7 @@ export function scanStagedContentForSecrets(cwd = process.cwd()) {
68
68
  return true;
69
69
  });
70
70
 
71
- for (const file of files.slice(0, 100)) { // giới hạn 100 file để nhanh
71
+ for (const file of files.slice(0, 100)) { // Limit to 100 files for speed.
72
72
  const content = readStagedContent(file, cwd);
73
73
  if (!content) continue;
74
74
  const lines = content.split('\n');
@@ -76,16 +76,16 @@ export function scanStagedContentForSecrets(cwd = process.cwd()) {
76
76
  for (let i = 0; i < lines.length; i++) {
77
77
  const line = lines[i];
78
78
  if (!line || line.length > 2000) continue;
79
- // Bỏ qua comment ví dụ trong docs? vẫn báo để user tự quyết
79
+ // Still report matches in documentation examples so the user can decide.
80
80
  for (const p of SECRET_PATTERNS) {
81
81
  if (p.regex.test(line)) {
82
82
  findings.push({ file, type: p.label, line: i + 1, preview: redactPreview(line) });
83
83
  matchesInFile++;
84
- break; // mỗi dòng chỉ báo 1 lần
84
+ break; // Report at most once per line.
85
85
  }
86
86
  }
87
87
  if (matchesInFile >= MAX_MATCHES_PER_FILE) break;
88
- if (findings.length >= 20) return findings; // chặn spam
88
+ if (findings.length >= 20) return findings; // Limit output to 20 findings.
89
89
  }
90
90
  }
91
91
  return findings;