codeant-cli 0.3.5 → 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.5",
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"
@@ -14,7 +14,7 @@ export default function Login() {
14
14
 
15
15
  useEffect(() => {
16
16
  // Check if already logged in
17
- const existingToken = getConfigValue('apiKey');
17
+ const existingToken = getConfigValue('apiKeyV2');
18
18
  if (existingToken) {
19
19
  setStatus('already_logged_in');
20
20
  setTimeout(() => exit(), 100);
@@ -48,7 +48,7 @@ export default function Login() {
48
48
  clearTimeout(timeoutId);
49
49
 
50
50
  // Save the API key
51
- setConfigValue('apiKey', token);
51
+ setConfigValue('apiKeyV2', token);
52
52
 
53
53
  setStatus('success');
54
54
  setTimeout(() => exit(), 100);
@@ -5,11 +5,11 @@ import { getConfigValue, setConfigValue } from '../utils/config.js';
5
5
  export default function Logout() {
6
6
  const { exit } = useApp();
7
7
 
8
- const wasLoggedIn = !!getConfigValue('apiKey');
8
+ const wasLoggedIn = !!getConfigValue('apiKeyV2');
9
9
 
10
10
  useEffect(() => {
11
11
  if (wasLoggedIn) {
12
- setConfigValue('apiKey', null);
12
+ setConfigValue('apiKeyV2', null);
13
13
  }
14
14
  exit();
15
15
  }, []);
@@ -31,7 +31,7 @@ export default function Review({ scanType = 'all', lastNCommits = 1, failOn = 'C
31
31
  const [fileCount, setFileCount] = useState(0);
32
32
  const [reviewMeta, setReviewMeta] = useState(null);
33
33
 
34
- const apiKey = getConfigValue('apiKey');
34
+ const apiKey = getConfigValue('apiKeyV2');
35
35
 
36
36
  useEffect(() => {
37
37
  if (!apiKey) {
@@ -87,8 +87,8 @@ export async function runReviewHeadless(options = {}) {
87
87
  } = options;
88
88
 
89
89
  // If the CLI config doesn't have an apiKey but the extension passed one, persist it
90
- if (apiKey && !getConfigValue('apiKey')) {
91
- setConfigValue('apiKey', apiKey);
90
+ if (apiKey && !getConfigValue('apiKeyV2')) {
91
+ setConfigValue('apiKeyV2', apiKey);
92
92
  }
93
93
 
94
94
  // Temporarily set env vars so fetchApi picks them up
@@ -24,7 +24,7 @@ function getClient() {
24
24
 
25
25
  /** Use the API key directly as distinct ID */
26
26
  function getDistinctId() {
27
- return process.env.CODEANT_API_TOKEN || getConfigValue('apiKey') || 'anonymous';
27
+ return process.env.CODEANT_API_TOKEN || getConfigValue('apiKeyV2') || 'anonymous';
28
28
  }
29
29
 
30
30
  /**
@@ -12,7 +12,7 @@ const fetchApi = async (endpoint, method = 'GET', body = null) => {
12
12
  };
13
13
 
14
14
  // Add auth token from config or env
15
- const token = process.env.CODEANT_API_TOKEN || getConfigValue('apiKey');
15
+ const token = process.env.CODEANT_API_TOKEN || getConfigValue('apiKeyV2');
16
16
  if (token) {
17
17
  options.headers['Authorization'] = `Bearer ${token}`;
18
18
  }
@@ -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
+ }