codeant-cli 0.1.0

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/README.md ADDED
@@ -0,0 +1,226 @@
1
+ # CodeAnt CLI
2
+
3
+ A command-line tool for code review and security scanning.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install -g codeant-cli
9
+ ```
10
+
11
+ Or run locally:
12
+
13
+ ```bash
14
+ git clone https://github.com/codeantai/codeant-cli.git
15
+ cd codeant-cli
16
+ npm install
17
+ npm link
18
+ ```
19
+
20
+ ## Quick Start
21
+
22
+ ```bash
23
+ # Login to CodeAnt
24
+ codeant login
25
+
26
+ # Scan staged files for secrets
27
+ codeant secrets
28
+ ```
29
+
30
+ ## Usage
31
+
32
+ ```bash
33
+ codeant <command> [options]
34
+ ```
35
+
36
+ ### Commands
37
+
38
+ #### `login`
39
+
40
+ Authenticate with CodeAnt. Opens a browser window for login.
41
+
42
+ ```bash
43
+ codeant login
44
+ ```
45
+
46
+ #### `logout`
47
+
48
+ Log out from CodeAnt.
49
+
50
+ ```bash
51
+ codeant logout
52
+ ```
53
+
54
+ #### `secrets`
55
+
56
+ Scan your code for exposed secrets, API keys, and credentials.
57
+
58
+ ```bash
59
+ codeant secrets [options]
60
+ ```
61
+
62
+ **Options:**
63
+
64
+ | Option | Description |
65
+ |--------|-------------|
66
+ | `--staged` | Scan only staged files (default) |
67
+ | `--all` | Scan all changed files compared to base branch |
68
+ | `--uncommitted` | Scan all uncommitted changes |
69
+ | `--last-commit` | Scan files from the last commit |
70
+ | `--fail-on <level>` | Fail only on HIGH, MEDIUM, or all (default: HIGH) |
71
+
72
+ **Examples:**
73
+
74
+ ```bash
75
+ # Scan staged files (default)
76
+ codeant secrets
77
+
78
+ # Scan all changed files
79
+ codeant secrets --all
80
+
81
+ # Scan last commit
82
+ codeant secrets --last-commit
83
+
84
+ # Only fail on HIGH confidence secrets (default)
85
+ codeant secrets --fail-on HIGH
86
+
87
+ # Fail on HIGH and MEDIUM confidence secrets
88
+ codeant secrets --fail-on MEDIUM
89
+
90
+ # Fail on all secrets (except false positives)
91
+ codeant secrets --fail-on all
92
+ ```
93
+
94
+ **Exit codes:**
95
+ - `0` - No blocking secrets found (or only false positives)
96
+ - `1` - Secrets detected that match the `--fail-on` threshold
97
+
98
+ **Confidence Levels:**
99
+ - `HIGH` - High confidence, likely a real secret
100
+ - `MEDIUM` - Medium confidence, may need review
101
+ - `FALSE_POSITIVE` - Detected but likely not a real secret (always ignored)
102
+
103
+ #### `set-base-url <url>`
104
+
105
+ Set a custom API base URL.
106
+
107
+ ```bash
108
+ codeant set-base-url https://api.example.com
109
+ ```
110
+
111
+ #### `get-base-url`
112
+
113
+ Show the current API base URL and its source.
114
+
115
+ ```bash
116
+ codeant get-base-url
117
+ ```
118
+
119
+ ### Global Options
120
+
121
+ ```bash
122
+ codeant --version # Show version
123
+ codeant --help # Show help
124
+ ```
125
+
126
+ ## Configuration
127
+
128
+ Config is stored in `~/.codeant/config.json`.
129
+
130
+ You can also use environment variables:
131
+
132
+ | Variable | Description |
133
+ |----------|-------------|
134
+ | `CODEANT_API_URL` | API base URL (overrides config) |
135
+ | `CODEANT_API_TOKEN` | Authentication token (overrides config) |
136
+
137
+ **Priority order:**
138
+ 1. Environment variables (highest)
139
+ 2. Config file (`~/.codeant/config.json`)
140
+ 3. Default values
141
+
142
+ ## Git Hooks
143
+
144
+ Use CodeAnt as a pre-commit hook to prevent secrets from being committed.
145
+
146
+ ### Manual Setup
147
+
148
+ Create `.git/hooks/pre-commit`:
149
+
150
+ ```bash
151
+ #!/bin/sh
152
+ codeant secrets
153
+ ```
154
+
155
+ Make it executable:
156
+
157
+ ```bash
158
+ chmod +x .git/hooks/pre-commit
159
+ ```
160
+
161
+ ### With Husky
162
+
163
+ ```bash
164
+ npx husky add .husky/pre-commit "codeant secrets"
165
+ ```
166
+
167
+ ### With lefthook
168
+
169
+ Add to `lefthook.yml`:
170
+
171
+ ```yaml
172
+ pre-commit:
173
+ commands:
174
+ secrets:
175
+ run: codeant secrets
176
+ ```
177
+
178
+ ## Example Output
179
+
180
+ ### Secrets Found (blocking)
181
+
182
+ ```
183
+ ✗ 2 secret(s) found!
184
+
185
+ src/config.js
186
+ Line 5: AWS Access Key (HIGH)
187
+ Line 12: API Key (HIGH)
188
+
189
+ Remove secrets before committing.
190
+ ```
191
+
192
+ ### Only False Positives (non-blocking)
193
+
194
+ ```
195
+ ⚠ 1 potential secret(s) found (ignored)
196
+
197
+ Ignored (false positives):
198
+ src/example.js
199
+ Line 10: Generic Secret (FALSE_POSITIVE)
200
+
201
+ ✓ Commit allowed (only false positives found)
202
+ ```
203
+
204
+ ### No Secrets
205
+
206
+ ```
207
+ ✓ No secrets found
208
+ ```
209
+
210
+ ## Development
211
+
212
+ ```bash
213
+ # Run locally
214
+ node src/index.js secrets
215
+
216
+ # Run with npm
217
+ npm start secrets
218
+
219
+ # Test different scan types
220
+ node src/index.js secrets --last-commit
221
+ node src/index.js secrets --all
222
+ ```
223
+
224
+ ## License
225
+
226
+ MIT
package/package.json ADDED
@@ -0,0 +1,28 @@
1
+ {
2
+ "name": "codeant-cli",
3
+ "version": "0.1.0",
4
+ "description": "Code review CLI tool",
5
+ "type": "module",
6
+ "bin": {
7
+ "codeant": "./src/index.js"
8
+ },
9
+ "scripts": {
10
+ "start": "node src/index.js"
11
+ },
12
+ "keywords": ["cli", "code-review", "secrets"],
13
+ "author": "",
14
+ "license": "MIT",
15
+ "repository": {
16
+ "type": "git",
17
+ "url": ""
18
+ },
19
+ "files": [
20
+ "src"
21
+ ],
22
+ "dependencies": {
23
+ "commander": "^12.1.0",
24
+ "ink": "^5.0.1",
25
+ "open": "^10.1.0",
26
+ "react": "^18.3.1"
27
+ }
28
+ }
@@ -0,0 +1,25 @@
1
+ import React, { useEffect } from 'react';
2
+ import { Text, Box, useApp } from 'ink';
3
+ import { getConfigValue } from '../utils/config.js';
4
+
5
+ export default function GetBaseUrl() {
6
+ const { exit } = useApp();
7
+
8
+ const envUrl = process.env.CODEANT_API_URL;
9
+ const configUrl = getConfigValue('baseUrl');
10
+ const defaultUrl = 'https://api.codeant.ai';
11
+
12
+ const activeUrl = envUrl || configUrl || defaultUrl;
13
+ const source = envUrl ? 'env' : configUrl ? 'config' : 'default';
14
+
15
+ useEffect(() => {
16
+ exit();
17
+ }, []);
18
+
19
+ return React.createElement(
20
+ Box,
21
+ { flexDirection: 'column', padding: 1 },
22
+ React.createElement(Text, { bold: true }, 'Base URL: ', activeUrl),
23
+ React.createElement(Text, { color: 'gray' }, 'Source: ', source)
24
+ );
25
+ }
@@ -0,0 +1,124 @@
1
+ import React, { useState, useEffect } from 'react';
2
+ import { Text, Box, useApp } from 'ink';
3
+ import { getConfigValue, setConfigValue } from '../utils/config.js';
4
+ import { randomUUID } from 'crypto';
5
+
6
+ const POLL_INTERVAL = 10000; // 10 seconds
7
+ const TIMEOUT = 10 * 60 * 1000; // 10 minutes
8
+
9
+ export default function Login() {
10
+ const { exit } = useApp();
11
+ const [status, setStatus] = useState('opening');
12
+ const [error, setError] = useState(null);
13
+
14
+ useEffect(() => {
15
+ // Check if already logged in
16
+ const existingToken = getConfigValue('apiKey');
17
+ if (existingToken) {
18
+ setStatus('already_logged_in');
19
+ setTimeout(() => exit(), 100);
20
+ return;
21
+ }
22
+
23
+ const token = randomUUID();
24
+ const baseUrl = getConfigValue('baseUrl') || 'https://api.codeant.ai';
25
+ const loginUrl = `https://app.codeant.ai?ideLoginToken=${token}`;
26
+ const pollUrl = `${baseUrl}/extension/login/status?apiKey=${token}`;
27
+
28
+ // Open browser
29
+ import('open').then(({ default: open }) => {
30
+ open(loginUrl);
31
+ setStatus('waiting');
32
+ }).catch(() => {
33
+ // Fallback: just show the URL
34
+ console.log(`\nOpen this URL in your browser:\n${loginUrl}\n`);
35
+ setStatus('waiting');
36
+ });
37
+
38
+ // Poll for login status
39
+ let timeoutId;
40
+ const intervalId = setInterval(async () => {
41
+ try {
42
+ const response = await fetch(pollUrl);
43
+ const data = await response.json();
44
+
45
+ if (data.status === 'yes') {
46
+ clearInterval(intervalId);
47
+ clearTimeout(timeoutId);
48
+
49
+ // Save the API key
50
+ setConfigValue('apiKey', token);
51
+
52
+ // Check for custom API URL based on email domain
53
+ if (data.item?.email?.includes('commvault.com')) {
54
+ setConfigValue('baseUrl', 'https://codeant-api.commvault.com');
55
+ }
56
+
57
+ setStatus('success');
58
+ setTimeout(() => exit(), 100);
59
+ }
60
+ } catch (err) {
61
+ // Silently continue polling
62
+ }
63
+ }, POLL_INTERVAL);
64
+
65
+ // Timeout after 10 minutes
66
+ timeoutId = setTimeout(() => {
67
+ clearInterval(intervalId);
68
+ setError('Login timed out. Please try again.');
69
+ setStatus('error');
70
+ setTimeout(() => exit(new Error('Login timed out')), 100);
71
+ }, TIMEOUT);
72
+
73
+ // Cleanup
74
+ return () => {
75
+ clearInterval(intervalId);
76
+ clearTimeout(timeoutId);
77
+ };
78
+ }, []);
79
+
80
+ if (status === 'already_logged_in') {
81
+ return React.createElement(
82
+ Box,
83
+ { flexDirection: 'column', padding: 1 },
84
+ React.createElement(Text, { color: 'yellow' }, 'Already logged in.'),
85
+ React.createElement(Text, { color: 'gray' }, 'Run "codeant logout" first to re-authenticate.')
86
+ );
87
+ }
88
+
89
+ if (status === 'opening') {
90
+ return React.createElement(
91
+ Box,
92
+ { flexDirection: 'column', padding: 1 },
93
+ React.createElement(Text, null, 'Opening browser...')
94
+ );
95
+ }
96
+
97
+ if (status === 'waiting') {
98
+ return React.createElement(
99
+ Box,
100
+ { flexDirection: 'column', padding: 1 },
101
+ React.createElement(Text, { color: 'cyan' }, 'Waiting for login...'),
102
+ React.createElement(Text, { color: 'gray' }, 'Complete the login in your browser.'),
103
+ React.createElement(Text, { color: 'gray' }, 'Checking every 10 seconds. Timeout in 10 minutes.')
104
+ );
105
+ }
106
+
107
+ if (status === 'success') {
108
+ return React.createElement(
109
+ Box,
110
+ { flexDirection: 'column', padding: 1 },
111
+ React.createElement(Text, { color: 'green' }, '✓ Login successful!')
112
+ );
113
+ }
114
+
115
+ if (status === 'error') {
116
+ return React.createElement(
117
+ Box,
118
+ { flexDirection: 'column', padding: 1 },
119
+ React.createElement(Text, { color: 'red' }, '✗ ', error)
120
+ );
121
+ }
122
+
123
+ return null;
124
+ }
@@ -0,0 +1,30 @@
1
+ import React, { useEffect } from 'react';
2
+ import { Text, Box, useApp } from 'ink';
3
+ import { getConfigValue, setConfigValue } from '../utils/config.js';
4
+
5
+ export default function Logout() {
6
+ const { exit } = useApp();
7
+
8
+ const wasLoggedIn = !!getConfigValue('apiKey');
9
+
10
+ useEffect(() => {
11
+ if (wasLoggedIn) {
12
+ setConfigValue('apiKey', null);
13
+ }
14
+ exit();
15
+ }, []);
16
+
17
+ if (!wasLoggedIn) {
18
+ return React.createElement(
19
+ Box,
20
+ { flexDirection: 'column', padding: 1 },
21
+ React.createElement(Text, { color: 'yellow' }, 'Not logged in.')
22
+ );
23
+ }
24
+
25
+ return React.createElement(
26
+ Box,
27
+ { flexDirection: 'column', padding: 1 },
28
+ React.createElement(Text, { color: 'green' }, '✓ Logged out successfully.')
29
+ );
30
+ }
@@ -0,0 +1,251 @@
1
+ import React, { useState, useEffect } from 'react';
2
+ import { Text, Box, useApp } from 'ink';
3
+ import { getConfigValue } from '../utils/config.js';
4
+ import { fetchApi } from '../utils/fetchApi.js';
5
+ import SecretsApiHelper from '../utils/secretsApiHelper.js';
6
+
7
+ export default function Secrets({ scanType = 'staged-only', failOn = 'HIGH' }) {
8
+ const { exit } = useApp();
9
+ const [status, setStatus] = useState('initializing');
10
+ const [secrets, setSecrets] = useState([]);
11
+ const [error, setError] = useState(null);
12
+ const [fileCount, setFileCount] = useState(0);
13
+
14
+ const apiKey = getConfigValue('apiKey');
15
+
16
+ // Check if logged in
17
+ if (!apiKey) {
18
+ useEffect(() => {
19
+ exit(new Error('Not logged in'));
20
+ }, []);
21
+
22
+ return React.createElement(
23
+ Box,
24
+ { flexDirection: 'column', padding: 1 },
25
+ React.createElement(Text, { color: 'red' }, '✗ Not logged in.'),
26
+ React.createElement(Text, { color: 'gray' }, 'Run "codeant login" to authenticate.')
27
+ );
28
+ }
29
+
30
+ // Helper to check if a secret should cause failure based on failOn level
31
+ const shouldFailOn = (confidenceScore) => {
32
+ const score = confidenceScore?.toUpperCase();
33
+ if (score === 'FALSE_POSITIVE') return false;
34
+ if (failOn === 'HIGH') return score === 'HIGH';
35
+ if (failOn === 'MEDIUM') return score === 'HIGH' || score === 'MEDIUM';
36
+ return true; // 'all' - fail on any non-false-positive
37
+ };
38
+
39
+ useEffect(() => {
40
+ async function scanSecrets() {
41
+ try {
42
+ setStatus('scanning');
43
+
44
+ // Initialize git helper and get staged files
45
+ const helper = new SecretsApiHelper(process.cwd());
46
+ await helper.init();
47
+
48
+ const requestBody = await helper.buildSecretsApiRequest(scanType);
49
+ setFileCount(requestBody.files.length);
50
+
51
+ if (requestBody.files.length === 0) {
52
+ setStatus('no_files');
53
+ return;
54
+ }
55
+
56
+ // Call the secrets detection API
57
+ const response = await fetchApi(
58
+ '/extension/pr-review/secrets-detection',
59
+ 'POST',
60
+ requestBody
61
+ );
62
+
63
+ const detectedSecrets = response.secretsDetected || [];
64
+
65
+ // Filter to only include files with actual secrets
66
+ const filesWithSecrets = detectedSecrets.filter(
67
+ file => file.secrets && file.secrets.length > 0
68
+ );
69
+
70
+ setSecrets(filesWithSecrets);
71
+ setStatus('done');
72
+ } catch (err) {
73
+ setError(err.message);
74
+ setStatus('error');
75
+ }
76
+ }
77
+
78
+ scanSecrets();
79
+ }, []);
80
+
81
+ // Handle exit after status changes
82
+ useEffect(() => {
83
+ if (status === 'done') {
84
+ // Check if any secrets should cause failure based on failOn level
85
+ const hasBlockingSecrets = secrets.some(file =>
86
+ file.secrets.some(secret => shouldFailOn(secret.confidence_score))
87
+ );
88
+
89
+ if (hasBlockingSecrets) {
90
+ setTimeout(() => {
91
+ process.exitCode = 1;
92
+ exit(new Error('Secrets detected'));
93
+ }, 100);
94
+ } else {
95
+ setTimeout(() => exit(), 100);
96
+ }
97
+ } else if (status === 'no_files') {
98
+ setTimeout(() => exit(), 100);
99
+ } else if (status === 'error') {
100
+ setTimeout(() => exit(new Error(error)), 100);
101
+ }
102
+ }, [status, secrets]);
103
+
104
+ // Render: Initializing
105
+ if (status === 'initializing') {
106
+ return React.createElement(
107
+ Box,
108
+ { flexDirection: 'column', padding: 1 },
109
+ React.createElement(Text, { color: 'cyan' }, 'Initializing...')
110
+ );
111
+ }
112
+
113
+ // Render: Scanning
114
+ if (status === 'scanning') {
115
+ return React.createElement(
116
+ Box,
117
+ { flexDirection: 'column', padding: 1 },
118
+ React.createElement(Text, { color: 'cyan' }, `Scanning ${fileCount} file(s) for secrets...`)
119
+ );
120
+ }
121
+
122
+ // Render: No files to scan
123
+ if (status === 'no_files') {
124
+ const noFilesMessages = {
125
+ 'staged-only': 'Stage some changes first with "git add".',
126
+ 'branch-diff': 'No changes found compared to the base branch.',
127
+ 'uncommitted': 'No uncommitted changes found.',
128
+ 'last-commit': 'No files found in the last commit.'
129
+ };
130
+
131
+ return React.createElement(
132
+ Box,
133
+ { flexDirection: 'column', padding: 1 },
134
+ React.createElement(Text, { color: 'yellow' }, 'No files to scan.'),
135
+ React.createElement(Text, { color: 'gray' }, noFilesMessages[scanType] || 'No files found.')
136
+ );
137
+ }
138
+
139
+ // Render: Error
140
+ if (status === 'error') {
141
+ return React.createElement(
142
+ Box,
143
+ { flexDirection: 'column', padding: 1 },
144
+ React.createElement(Text, { color: 'red' }, '✗ Error: ', error)
145
+ );
146
+ }
147
+
148
+ // Render: Done with secrets found
149
+ if (status === 'done' && secrets.length > 0) {
150
+ const allSecrets = secrets.flatMap(file =>
151
+ file.secrets.map(s => ({ ...s, file_path: file.file_path }))
152
+ );
153
+
154
+ const blockingSecrets = allSecrets.filter(s => shouldFailOn(s.confidence_score));
155
+ const falsePositives = allSecrets.filter(s => !shouldFailOn(s.confidence_score));
156
+
157
+ const hasBlockingSecrets = blockingSecrets.length > 0;
158
+
159
+ // Group by file for display
160
+ const groupByFile = (secretsList) => {
161
+ const grouped = {};
162
+ secretsList.forEach(s => {
163
+ if (!grouped[s.file_path]) grouped[s.file_path] = [];
164
+ grouped[s.file_path].push(s);
165
+ });
166
+ return grouped;
167
+ };
168
+
169
+ const blockingByFile = groupByFile(blockingSecrets);
170
+ const falsePositivesByFile = groupByFile(falsePositives);
171
+
172
+ const elements = [
173
+ // Header
174
+ React.createElement(
175
+ Text,
176
+ { key: 'header', color: hasBlockingSecrets ? 'red' : 'yellow', bold: true },
177
+ hasBlockingSecrets
178
+ ? `✗ ${blockingSecrets.length} secret(s) found!`
179
+ : `⚠ ${falsePositives.length} potential secret(s) found (ignored)`
180
+ ),
181
+ React.createElement(Text, { key: 'spacer1' }, '')
182
+ ];
183
+
184
+ // Blocking secrets
185
+ if (blockingSecrets.length > 0) {
186
+ Object.entries(blockingByFile).forEach(([filePath, fileSecrets]) => {
187
+ elements.push(
188
+ React.createElement(Text, { key: `file-${filePath}`, color: 'yellow', bold: true }, filePath)
189
+ );
190
+ fileSecrets.forEach((secret, idx) => {
191
+ elements.push(
192
+ React.createElement(
193
+ Text,
194
+ { key: `secret-${filePath}-${idx}`, color: 'red' },
195
+ ` Line ${secret.line_number}: ${secret.type} (${secret.confidence_score})`
196
+ )
197
+ );
198
+ });
199
+ });
200
+ }
201
+
202
+ // False positives / ignored
203
+ if (falsePositives.length > 0) {
204
+ if (blockingSecrets.length > 0) {
205
+ elements.push(React.createElement(Text, { key: 'spacer2' }, ''));
206
+ }
207
+ elements.push(
208
+ React.createElement(Text, { key: 'fp-header', color: 'gray' }, 'Ignored (false positives):')
209
+ );
210
+ Object.entries(falsePositivesByFile).forEach(([filePath, fileSecrets]) => {
211
+ elements.push(
212
+ React.createElement(Text, { key: `fp-file-${filePath}`, color: 'gray' }, ` ${filePath}`)
213
+ );
214
+ fileSecrets.forEach((secret, idx) => {
215
+ elements.push(
216
+ React.createElement(
217
+ Text,
218
+ { key: `fp-secret-${filePath}-${idx}`, color: 'gray', dimColor: true },
219
+ ` Line ${secret.line_number}: ${secret.type} (${secret.confidence_score})`
220
+ )
221
+ );
222
+ });
223
+ });
224
+ }
225
+
226
+ elements.push(React.createElement(Text, { key: 'spacer3' }, ''));
227
+
228
+ if (hasBlockingSecrets) {
229
+ elements.push(
230
+ React.createElement(Text, { key: 'footer', color: 'gray' }, 'Remove secrets before committing.')
231
+ );
232
+ } else {
233
+ elements.push(
234
+ React.createElement(Text, { key: 'footer', color: 'green' }, '✓ Commit allowed (only false positives found)')
235
+ );
236
+ }
237
+
238
+ return React.createElement(
239
+ Box,
240
+ { flexDirection: 'column', padding: 1 },
241
+ ...elements
242
+ );
243
+ }
244
+
245
+ // Render: Done with no secrets
246
+ return React.createElement(
247
+ Box,
248
+ { flexDirection: 'column', padding: 1 },
249
+ React.createElement(Text, { color: 'green' }, '✓ No secrets found')
250
+ );
251
+ }