codeant-cli 0.3.6 → 0.3.7
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "codeant-cli",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.7",
|
|
4
4
|
"description": "Code review CLI tool",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -25,7 +25,8 @@
|
|
|
25
25
|
"main": "./src/reviewHeadless.js",
|
|
26
26
|
"exports": {
|
|
27
27
|
".": "./src/reviewHeadless.js",
|
|
28
|
-
"./review": "./src/reviewHeadless.js"
|
|
28
|
+
"./review": "./src/reviewHeadless.js",
|
|
29
|
+
"./push-protection": "./src/utils/installPushProtectionHook.js"
|
|
29
30
|
},
|
|
30
31
|
"files": [
|
|
31
32
|
"src"
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { execSync } from 'child_process';
|
|
2
|
+
import { readFileSync, writeFileSync, chmodSync, existsSync, unlinkSync, mkdirSync } from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
|
|
5
|
+
const HOOK_MARKER = '# codeant-push-protection';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Build the full pre-push hook script (with shebang).
|
|
9
|
+
* @param {string} failOn - Severity threshold (e.g. "HIGH", "CRITICAL")
|
|
10
|
+
*/
|
|
11
|
+
function buildHookScript(failOn = 'HIGH') {
|
|
12
|
+
return `#!/bin/sh
|
|
13
|
+
${buildHookBlock(failOn)}
|
|
14
|
+
`;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Build just the CodeAnt block (no shebang), used when appending to an existing hook.
|
|
19
|
+
* @param {string} failOn - Severity threshold
|
|
20
|
+
*/
|
|
21
|
+
function buildHookBlock(failOn = 'HIGH') {
|
|
22
|
+
return `${HOOK_MARKER}
|
|
23
|
+
# Auto-installed by CodeAnt AI — blocks pushes containing secrets.
|
|
24
|
+
# To disable: delete this hook or run "codeant push-protection disable"
|
|
25
|
+
command -v codeant >/dev/null 2>&1 || exit 0
|
|
26
|
+
codeant secrets --committed --fail-on ${failOn}
|
|
27
|
+
${HOOK_MARKER_END}`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const HOOK_MARKER_END = '# end-codeant-push-protection';
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Replace the CodeAnt block in a hook file with new content (or remove it).
|
|
34
|
+
*/
|
|
35
|
+
function replaceCodeAntBlock(fileContent, newBlock) {
|
|
36
|
+
const startIdx = fileContent.indexOf(HOOK_MARKER);
|
|
37
|
+
let endIdx = fileContent.indexOf(HOOK_MARKER_END);
|
|
38
|
+
if (startIdx === -1) return fileContent;
|
|
39
|
+
if (endIdx === -1) {
|
|
40
|
+
// Legacy hook without end marker — remove from start marker to EOF
|
|
41
|
+
endIdx = fileContent.length;
|
|
42
|
+
} else {
|
|
43
|
+
endIdx += HOOK_MARKER_END.length;
|
|
44
|
+
}
|
|
45
|
+
const before = fileContent.slice(0, startIdx);
|
|
46
|
+
const after = fileContent.slice(endIdx);
|
|
47
|
+
return (before + newBlock + after).replace(/\n{3,}/g, '\n\n');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Find the git root directory for a given workspace path.
|
|
52
|
+
*/
|
|
53
|
+
function findGitRoot(workspacePath) {
|
|
54
|
+
try {
|
|
55
|
+
return execSync('git rev-parse --show-toplevel', {
|
|
56
|
+
cwd: workspacePath,
|
|
57
|
+
encoding: 'utf-8',
|
|
58
|
+
timeout: 5000,
|
|
59
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
60
|
+
}).trim();
|
|
61
|
+
} catch {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Get the effective hooks directory (respects core.hooksPath).
|
|
68
|
+
*/
|
|
69
|
+
function getHooksDir(gitRoot) {
|
|
70
|
+
try {
|
|
71
|
+
const custom = execSync('git config --get core.hooksPath', {
|
|
72
|
+
cwd: gitRoot,
|
|
73
|
+
encoding: 'utf-8',
|
|
74
|
+
timeout: 5000,
|
|
75
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
76
|
+
}).trim();
|
|
77
|
+
if (custom) return path.resolve(gitRoot, custom);
|
|
78
|
+
} catch {
|
|
79
|
+
// No custom hooksPath — use default
|
|
80
|
+
}
|
|
81
|
+
return path.join(gitRoot, '.git', 'hooks');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Install a pre-push hook that runs secret scanning before push.
|
|
86
|
+
*
|
|
87
|
+
* @param {string} workspacePath - Path to the git repository
|
|
88
|
+
* @param {object} [options]
|
|
89
|
+
* @param {string} [options.failOn="HIGH"] - Severity threshold
|
|
90
|
+
* @returns {{ installed: boolean, hookPath: string|null, message: string }}
|
|
91
|
+
*/
|
|
92
|
+
export function installPushProtectionHook(workspacePath, options = {}) {
|
|
93
|
+
const { failOn = 'HIGH' } = options;
|
|
94
|
+
|
|
95
|
+
const gitRoot = findGitRoot(workspacePath);
|
|
96
|
+
if (!gitRoot) {
|
|
97
|
+
return { installed: false, hookPath: null, message: 'Not a git repository' };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const hooksDir = getHooksDir(gitRoot);
|
|
101
|
+
if (!existsSync(hooksDir)) {
|
|
102
|
+
mkdirSync(hooksDir, { recursive: true });
|
|
103
|
+
}
|
|
104
|
+
const hookPath = path.join(hooksDir, 'pre-push');
|
|
105
|
+
|
|
106
|
+
// If hook already exists, check if it's ours
|
|
107
|
+
if (existsSync(hookPath)) {
|
|
108
|
+
const existing = readFileSync(hookPath, 'utf-8');
|
|
109
|
+
if (existing.includes(HOOK_MARKER)) {
|
|
110
|
+
// Replace only our block, preserve everything else
|
|
111
|
+
const updated = replaceCodeAntBlock(existing, buildHookBlock(failOn));
|
|
112
|
+
writeFileSync(hookPath, updated, 'utf-8');
|
|
113
|
+
chmodSync(hookPath, 0o755);
|
|
114
|
+
return { installed: true, hookPath, message: 'Hook updated' };
|
|
115
|
+
}
|
|
116
|
+
// There's a user-managed hook — append our block (no duplicate shebang)
|
|
117
|
+
const appended = existing.trimEnd() + '\n\n' + buildHookBlock(failOn) + '\n';
|
|
118
|
+
writeFileSync(hookPath, appended, 'utf-8');
|
|
119
|
+
chmodSync(hookPath, 0o755);
|
|
120
|
+
return { installed: true, hookPath, message: 'Hook appended to existing pre-push' };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
writeFileSync(hookPath, buildHookScript(failOn), 'utf-8');
|
|
124
|
+
chmodSync(hookPath, 0o755);
|
|
125
|
+
return { installed: true, hookPath, message: 'Hook installed' };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Remove the CodeAnt pre-push hook (or just our section if appended).
|
|
130
|
+
*
|
|
131
|
+
* @param {string} workspacePath
|
|
132
|
+
* @returns {{ removed: boolean, message: string }}
|
|
133
|
+
*/
|
|
134
|
+
export function removePushProtectionHook(workspacePath) {
|
|
135
|
+
const gitRoot = findGitRoot(workspacePath);
|
|
136
|
+
if (!gitRoot) {
|
|
137
|
+
return { removed: false, message: 'Not a git repository' };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const hooksDir = getHooksDir(gitRoot);
|
|
141
|
+
const hookPath = path.join(hooksDir, 'pre-push');
|
|
142
|
+
|
|
143
|
+
if (!existsSync(hookPath)) {
|
|
144
|
+
return { removed: false, message: 'No pre-push hook found' };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const content = readFileSync(hookPath, 'utf-8');
|
|
148
|
+
if (!content.includes(HOOK_MARKER)) {
|
|
149
|
+
return { removed: false, message: 'Hook is not managed by CodeAnt' };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Remove our block (between start and end markers)
|
|
153
|
+
const remaining = replaceCodeAntBlock(content, '').trim();
|
|
154
|
+
if (!remaining || remaining === '#!/bin/sh') {
|
|
155
|
+
// Nothing left — delete the file
|
|
156
|
+
unlinkSync(hookPath);
|
|
157
|
+
return { removed: true, message: 'Hook removed' };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
writeFileSync(hookPath, remaining + '\n', 'utf-8');
|
|
161
|
+
chmodSync(hookPath, 0o755);
|
|
162
|
+
return { removed: true, message: 'CodeAnt section removed from hook' };
|
|
163
|
+
}
|