Skip to content

Commit 660d7d5

Browse files
authored
Merge pull request #4 from CodeAnt-AI/feat/set-telemetry
pre push hook
2 parents b2c4467 + a2700dd commit 660d7d5

File tree

3 files changed

+169
-2
lines changed

3 files changed

+169
-2
lines changed

changelog.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
# Changelog
22

3+
## [0.3.7] - 03/04/2026
4+
- Pre-push hook
5+
36
## [0.3.6] - 02/04/2026
47
- new login approach
58

package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "codeant-cli",
3-
"version": "0.3.6",
3+
"version": "0.3.7",
44
"description": "Code review CLI tool",
55
"type": "module",
66
"bin": {
@@ -25,7 +25,8 @@
2525
"main": "./src/reviewHeadless.js",
2626
"exports": {
2727
".": "./src/reviewHeadless.js",
28-
"./review": "./src/reviewHeadless.js"
28+
"./review": "./src/reviewHeadless.js",
29+
"./push-protection": "./src/utils/installPushProtectionHook.js"
2930
},
3031
"files": [
3132
"src"
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
}

0 commit comments

Comments
 (0)