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.
- package/.github/workflows/npm-publish.yml +59 -32
- package/README.md +110 -112
- package/bin/index.js +321 -339
- package/lib/commands/edit-time.js +141 -141
- package/lib/commands/fix.js +113 -113
- package/lib/commands/github.js +429 -445
- package/lib/commands/pull.js +90 -90
- package/lib/commands/push.js +720 -720
- package/lib/config.js +220 -220
- package/lib/fix.js +286 -286
- package/lib/git.js +525 -516
- package/lib/github.js +322 -322
- package/lib/prompt-helpers.js +19 -20
- package/lib/secrets.js +92 -92
- package/lib/ui.js +1057 -1063
- package/lib/utils.js +120 -120
- package/package.json +36 -36
package/lib/prompt-helpers.js
CHANGED
|
@@ -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')('«
|
|
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
|
|
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
|
-
//
|
|
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
|
-
*
|
|
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('
|
|
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
|
-
*
|
|
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: '«
|
|
126
|
+
{ name: '« Go back (previous step)', value: BACK, description: 'Return to the previous step or main menu' }
|
|
128
127
|
];
|
|
129
|
-
const backHint = hint ?? '↑↓
|
|
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
|
-
*
|
|
133
|
+
* Render an input prompt inside a frame with a header and answer footer.
|
|
135
134
|
*/
|
|
136
|
-
export async function inputBox({ message, promptLabel = '
|
|
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(`
|
|
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
|
-
*
|
|
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 ?? '
|
|
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
|
-
*
|
|
181
|
+
* Ask whether to continue, return to the menu, or exit.
|
|
183
182
|
*/
|
|
184
|
-
export async function promptContinueOrBack(message = '
|
|
183
|
+
export async function promptContinueOrBack(message = 'Would you like to continue or go back?') {
|
|
185
184
|
return await selectBox({
|
|
186
185
|
message,
|
|
187
|
-
hint: '
|
|
186
|
+
hint: 'Choose what to do next',
|
|
188
187
|
choices: [
|
|
189
|
-
{ name: '
|
|
190
|
-
{ name: '
|
|
191
|
-
{ name: '
|
|
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
|
-
//
|
|
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: '
|
|
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; //
|
|
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
|
-
//
|
|
37
|
-
//
|
|
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; //
|
|
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
|
-
//
|
|
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
|
-
*
|
|
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)) { //
|
|
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
|
-
//
|
|
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; //
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
if (matchesInFile >= MAX_MATCHES_PER_FILE) break;
|
|
88
|
-
if (findings.length >= 20) return findings; //
|
|
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
|
+
}
|