codeant-cli 0.1.0 → 0.1.2

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 CHANGED
@@ -68,6 +68,8 @@ codeant secrets [options]
68
68
  | `--uncommitted` | Scan all uncommitted changes |
69
69
  | `--last-commit` | Scan files from the last commit |
70
70
  | `--fail-on <level>` | Fail only on HIGH, MEDIUM, or all (default: HIGH) |
71
+ | `--include <patterns>` | Comma-separated glob patterns to include files |
72
+ | `--exclude <patterns>` | Comma-separated glob patterns to exclude files |
71
73
 
72
74
  **Examples:**
73
75
 
@@ -89,8 +91,21 @@ codeant secrets --fail-on MEDIUM
89
91
 
90
92
  # Fail on all secrets (except false positives)
91
93
  codeant secrets --fail-on all
94
+
95
+ # Filter files using glob patterns
96
+ codeant secrets --include '**/*.js' # Only JS files
97
+ codeant secrets --exclude 'node_modules/**,*.test.js' # Exclude patterns
98
+ codeant secrets --include 'src/**' --exclude '*.test.*' # Combine both
92
99
  ```
93
100
 
101
+ **File Filtering:**
102
+
103
+ Use `--include` and `--exclude` with glob patterns to filter files:
104
+ - `*` matches any characters except `/`
105
+ - `**` matches any characters including `/`
106
+ - `*.{js,ts}` matches multiple extensions
107
+ - Comma-separated for multiple patterns: `--exclude 'test/**,dist/**'`
108
+
94
109
  **Exit codes:**
95
110
  - `0` - No blocking secrets found (or only false positives)
96
111
  - `1` - Secrets detected that match the `--fail-on` threshold
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codeant-cli",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Code review CLI tool",
5
5
  "type": "module",
6
6
  "bin": {
@@ -9,7 +9,11 @@
9
9
  "scripts": {
10
10
  "start": "node src/index.js"
11
11
  },
12
- "keywords": ["cli", "code-review", "secrets"],
12
+ "keywords": [
13
+ "cli",
14
+ "code-review",
15
+ "secrets"
16
+ ],
13
17
  "author": "",
14
18
  "license": "MIT",
15
19
  "repository": {
@@ -22,6 +26,7 @@
22
26
  "dependencies": {
23
27
  "commander": "^12.1.0",
24
28
  "ink": "^5.0.1",
29
+ "minimatch": "^10.1.1",
25
30
  "open": "^10.1.0",
26
31
  "react": "^18.3.1"
27
32
  }
@@ -4,7 +4,7 @@ import { getConfigValue } from '../utils/config.js';
4
4
  import { fetchApi } from '../utils/fetchApi.js';
5
5
  import SecretsApiHelper from '../utils/secretsApiHelper.js';
6
6
 
7
- export default function Secrets({ scanType = 'staged-only', failOn = 'HIGH' }) {
7
+ export default function Secrets({ scanType = 'staged-only', failOn = 'HIGH', include = [], exclude = [] }) {
8
8
  const { exit } = useApp();
9
9
  const [status, setStatus] = useState('initializing');
10
10
  const [secrets, setSecrets] = useState([]);
@@ -13,12 +13,16 @@ export default function Secrets({ scanType = 'staged-only', failOn = 'HIGH' }) {
13
13
 
14
14
  const apiKey = getConfigValue('apiKey');
15
15
 
16
+ // Handle not logged in state
17
+ useEffect(() => {
18
+ if (!apiKey) {
19
+ const id = setTimeout(() => exit(new Error('Not logged in')), 0);
20
+ return () => clearTimeout(id);
21
+ }
22
+ }, [apiKey, exit]);
23
+
16
24
  // Check if logged in
17
25
  if (!apiKey) {
18
- useEffect(() => {
19
- exit(new Error('Not logged in'));
20
- }, []);
21
-
22
26
  return React.createElement(
23
27
  Box,
24
28
  { flexDirection: 'column', padding: 1 },
@@ -37,19 +41,23 @@ export default function Secrets({ scanType = 'staged-only', failOn = 'HIGH' }) {
37
41
  };
38
42
 
39
43
  useEffect(() => {
44
+ let cancelled = false;
45
+
40
46
  async function scanSecrets() {
41
47
  try {
48
+ if (cancelled) return;
42
49
  setStatus('scanning');
43
50
 
44
51
  // Initialize git helper and get staged files
45
52
  const helper = new SecretsApiHelper(process.cwd());
46
53
  await helper.init();
47
54
 
48
- const requestBody = await helper.buildSecretsApiRequest(scanType);
49
- setFileCount(requestBody.files.length);
55
+ if (cancelled) return;
56
+ const requestBody = await helper.buildSecretsApiRequest(scanType, include, exclude);
57
+ if (cancelled) return;
50
58
 
51
59
  if (requestBody.files.length === 0) {
52
- setStatus('no_files');
60
+ if (!cancelled) setStatus('no_files');
53
61
  return;
54
62
  }
55
63
 
@@ -60,6 +68,7 @@ export default function Secrets({ scanType = 'staged-only', failOn = 'HIGH' }) {
60
68
  requestBody
61
69
  );
62
70
 
71
+ if (cancelled) return;
63
72
  const detectedSecrets = response.secretsDetected || [];
64
73
 
65
74
  // Filter to only include files with actual secrets
@@ -67,16 +76,24 @@ export default function Secrets({ scanType = 'staged-only', failOn = 'HIGH' }) {
67
76
  file => file.secrets && file.secrets.length > 0
68
77
  );
69
78
 
70
- setSecrets(filesWithSecrets);
71
- setStatus('done');
79
+ if (!cancelled) {
80
+ setSecrets(filesWithSecrets);
81
+ setStatus('done');
82
+ }
72
83
  } catch (err) {
73
- setError(err.message);
74
- setStatus('error');
84
+ if (!cancelled) {
85
+ setError(err.message);
86
+ setStatus('error');
87
+ }
75
88
  }
76
89
  }
77
90
 
78
91
  scanSecrets();
79
- }, []);
92
+
93
+ return () => {
94
+ cancelled = true;
95
+ };
96
+ }, [scanType, include, exclude]);
80
97
 
81
98
  // Handle exit after status changes
82
99
  useEffect(() => {
@@ -0,0 +1,143 @@
1
+ import React, { useState, useEffect } from 'react';
2
+ import { Text, Box, useApp } from 'ink';
3
+
4
+ const logo = `
5
+ ___ _ _ _
6
+ / __\\___ __| | ___ / \\ _ __ | |_
7
+ / / / _ \\ / _\` |/ _ \\/ /\\ | '_ \\| __|
8
+ / /__| (_) | (_| | __/\\_/ \\ | | | | |_
9
+ \\____/\\___/ \\__,_|\\___\\___/\\_/_| |_|\\__|
10
+ `;
11
+
12
+ const tagline = "Your AI-powered code review companion";
13
+
14
+ const commands = [
15
+ { cmd: 'codeant secrets', desc: 'Scan for secrets in your code' },
16
+ { cmd: 'codeant login', desc: 'Login to CodeAnt' },
17
+ { cmd: 'codeant --help', desc: 'Show all commands' },
18
+ ];
19
+
20
+ export default function Welcome() {
21
+ const { exit } = useApp();
22
+ const [displayedLogo, setDisplayedLogo] = useState('');
23
+ const [displayedTagline, setDisplayedTagline] = useState('');
24
+ const [showCursor, setShowCursor] = useState(true);
25
+ const [phase, setPhase] = useState('logo');
26
+ const [showCommands, setShowCommands] = useState(false);
27
+
28
+ // Typing effect for logo (fast)
29
+ useEffect(() => {
30
+ if (phase !== 'logo') return;
31
+
32
+ let i = 0;
33
+ const chars = logo.split('');
34
+ const interval = setInterval(() => {
35
+ if (i < chars.length) {
36
+ setDisplayedLogo(prev => prev + chars[i]);
37
+ i++;
38
+ } else {
39
+ clearInterval(interval);
40
+ setPhase('tagline');
41
+ }
42
+ }, 2);
43
+
44
+ return () => clearInterval(interval);
45
+ }, [phase]);
46
+
47
+ // Typing effect for tagline (slower, more dramatic)
48
+ useEffect(() => {
49
+ if (phase !== 'tagline') return;
50
+
51
+ let i = 0;
52
+ const chars = tagline.split('');
53
+ const interval = setInterval(() => {
54
+ if (i < chars.length) {
55
+ setDisplayedTagline(prev => prev + chars[i]);
56
+ i++;
57
+ } else {
58
+ clearInterval(interval);
59
+ setPhase('commands');
60
+ setShowCommands(true);
61
+ }
62
+ }, 25);
63
+
64
+ return () => clearInterval(interval);
65
+ }, [phase]);
66
+
67
+ // Blinking cursor
68
+ useEffect(() => {
69
+ if (phase === 'commands') {
70
+ setShowCursor(false);
71
+ return;
72
+ }
73
+ const interval = setInterval(() => {
74
+ setShowCursor(prev => !prev);
75
+ }, 400);
76
+ return () => clearInterval(interval);
77
+ }, [phase]);
78
+
79
+ // Exit after animation
80
+ useEffect(() => {
81
+ if (showCommands) {
82
+ const timeout = setTimeout(() => exit(), 100);
83
+ return () => clearTimeout(timeout);
84
+ }
85
+ }, [showCommands, exit]);
86
+
87
+ const cursor = showCursor ? '|' : ' ';
88
+
89
+ const elements = [
90
+ React.createElement(Text, { key: 'logo', color: 'cyan', bold: true }, displayedLogo)
91
+ ];
92
+
93
+ if (phase !== 'logo') {
94
+ elements.push(
95
+ React.createElement(
96
+ Box,
97
+ { key: 'tagline-box', marginTop: 0 },
98
+ React.createElement(
99
+ Text,
100
+ { color: 'magenta', italic: true },
101
+ displayedTagline,
102
+ React.createElement(Text, { color: 'gray' }, cursor)
103
+ )
104
+ )
105
+ );
106
+ }
107
+
108
+ if (showCommands) {
109
+ elements.push(
110
+ React.createElement(Text, { key: 'divider', color: 'gray' }, '─────────────────────────────────────────')
111
+ );
112
+
113
+ elements.push(
114
+ React.createElement(
115
+ Box,
116
+ { key: 'commands-box', marginTop: 1, flexDirection: 'column' },
117
+ React.createElement(Text, { color: 'yellow', bold: true }, 'Quick Start:'),
118
+ ...commands.map((item, idx) =>
119
+ React.createElement(
120
+ Box,
121
+ { key: `cmd-${idx}`, marginLeft: 2 },
122
+ React.createElement(Text, { color: 'green' }, `$ ${item.cmd}`),
123
+ React.createElement(Text, { color: 'gray' }, ` ${item.desc}`)
124
+ )
125
+ )
126
+ )
127
+ );
128
+
129
+ elements.push(
130
+ React.createElement(
131
+ Box,
132
+ { key: 'version-box', marginTop: 1 },
133
+ React.createElement(Text, { color: 'gray' }, 'v0.1.0')
134
+ )
135
+ );
136
+ }
137
+
138
+ return React.createElement(
139
+ Box,
140
+ { flexDirection: 'column', padding: 1 },
141
+ ...elements
142
+ );
143
+ }
package/src/index.js CHANGED
@@ -8,11 +8,16 @@ import SetBaseUrl from './commands/setBaseUrl.js';
8
8
  import GetBaseUrl from './commands/getBaseUrl.js';
9
9
  import Login from './commands/login.js';
10
10
  import Logout from './commands/logout.js';
11
+ import Welcome from './components/Welcome.js';
11
12
 
12
- program
13
- .name('codeant')
14
- .description('Code review CLI tool')
15
- .version('0.1.0');
13
+ // Show welcome animation if no arguments provided
14
+ if (process.argv.length === 2) {
15
+ render(React.createElement(Welcome));
16
+ } else {
17
+ program
18
+ .name('codeant')
19
+ .description('Code review CLI tool')
20
+ .version('0.1.0');
16
21
 
17
22
  program
18
23
  .command('secrets')
@@ -22,41 +27,54 @@ program
22
27
  .option('--uncommitted', 'Scan uncommitted changes')
23
28
  .option('--last-commit', 'Scan last commit')
24
29
  .option('--fail-on <level>', 'Fail only on HIGH, MEDIUM, or all (default: HIGH)', 'HIGH')
30
+ .option('--include <paths>', 'Comma-separated list of file paths regex to include')
31
+ .option('--exclude <paths>', 'Comma-separated list of file paths regex to exclude')
25
32
  .action((options) => {
26
33
  let scanType = 'staged-only';
27
34
  if (options.all) scanType = 'branch-diff';
28
35
  else if (options.uncommitted) scanType = 'uncommitted';
29
36
  else if (options.lastCommit) scanType = 'last-commit';
30
37
 
31
- render(React.createElement(Secrets, { scanType, failOn: options.failOn }));
32
- });
38
+ const include = options.include
39
+ ? (Array.isArray(options.include) ? options.include : options.include.split(',')).map(s => s.trim()).filter(Boolean)
40
+ : [];
33
41
 
34
- program
35
- .command('set-base-url <url>')
36
- .description('Set the API base URL')
37
- .action((url) => {
38
- render(React.createElement(SetBaseUrl, { url }));
39
- });
42
+ const exclude = options.exclude
43
+ ? (Array.isArray(options.exclude) ? options.exclude : options.exclude.split(',')).map(s => s.trim()).filter(Boolean)
44
+ : [];
40
45
 
41
- program
42
- .command('get-base-url')
43
- .description('Show the current API base URL')
44
- .action(() => {
45
- render(React.createElement(GetBaseUrl));
46
- });
46
+ const failOn = options.failOn?.toUpperCase() || 'HIGH';
47
47
 
48
- program
49
- .command('login')
50
- .description('Login to CodeAnt')
51
- .action(() => {
52
- render(React.createElement(Login));
48
+ render(React.createElement(Secrets, { scanType, failOn, include, exclude }));
53
49
  });
54
50
 
55
- program
56
- .command('logout')
57
- .description('Logout from CodeAnt')
58
- .action(() => {
59
- render(React.createElement(Logout));
60
- });
51
+ program
52
+ .command('set-base-url <url>')
53
+ .description('Set the API base URL')
54
+ .action((url) => {
55
+ render(React.createElement(SetBaseUrl, { url }));
56
+ });
57
+
58
+ program
59
+ .command('get-base-url')
60
+ .description('Show the current API base URL')
61
+ .action(() => {
62
+ render(React.createElement(GetBaseUrl));
63
+ });
64
+
65
+ program
66
+ .command('login')
67
+ .description('Login to CodeAnt')
68
+ .action(() => {
69
+ render(React.createElement(Login));
70
+ });
71
+
72
+ program
73
+ .command('logout')
74
+ .description('Logout from CodeAnt')
75
+ .action(() => {
76
+ render(React.createElement(Logout));
77
+ });
61
78
 
62
- program.parse();
79
+ program.parse();
80
+ }
@@ -1,4 +1,5 @@
1
1
  import GitDiffHelper from './gitDiffHelper.js';
2
+ import { minimatch } from 'minimatch';
2
3
 
3
4
  /**
4
5
  * Transforms git diff data into the format expected by the secrets detection API
@@ -93,10 +94,70 @@ class SecretsApiHelper {
93
94
  return Array.from(fileMap.values());
94
95
  }
95
96
 
97
+ /**
98
+ * Filter files based on include and exclude glob patterns
99
+ * @param {Array} files - Array of file objects with file_path property
100
+ * @param {Array} includePatterns - Array of glob pattern strings to include
101
+ * @param {Array} excludePatterns - Array of glob pattern strings to exclude
102
+ * @returns {Array} Filtered array of files
103
+ */
104
+ _filterFiles(files, includePatterns = [], excludePatterns = []) {
105
+ return files.filter(file => {
106
+ const filePath = file.file_path;
107
+
108
+ // If include patterns are specified, file must match at least one
109
+ if (includePatterns.length > 0) {
110
+ const matchesInclude = includePatterns.some(pattern => {
111
+ try {
112
+ // If pattern is a RegExp, test it directly against the file path.
113
+ if (pattern instanceof RegExp) {
114
+ return pattern.test(filePath);
115
+ }
116
+ // Use matchBase option to match basename patterns like '*.js' for glob strings
117
+ return minimatch(filePath, pattern, { matchBase: true });
118
+ } catch (e) {
119
+ console.warn(`Invalid include pattern: ${pattern}`, e.message);
120
+ return false;
121
+ }
122
+ });
123
+
124
+ if (!matchesInclude) {
125
+ return false;
126
+ }
127
+ }
128
+
129
+ // If exclude patterns are specified, file must not match any
130
+ if (excludePatterns.length > 0) {
131
+ const matchesExclude = excludePatterns.some(pattern => {
132
+ try {
133
+ // If pattern is a RegExp, test it directly against the file path.
134
+ if (pattern instanceof RegExp) {
135
+ return pattern.test(filePath);
136
+ }
137
+ // Use matchBase option to match basename patterns like '*.js' for glob strings
138
+ return minimatch(filePath, pattern, { matchBase: true });
139
+ } catch (e) {
140
+ console.warn(`Invalid exclude pattern: ${pattern}`, e.message);
141
+ return false;
142
+ }
143
+ });
144
+
145
+ if (matchesExclude) {
146
+ return false;
147
+ }
148
+ }
149
+
150
+ return true;
151
+ });
152
+ }
153
+
96
154
  /**
97
155
  * Build the complete request body for the secrets API
156
+ * @param {string} type - Type of scan (staged-only, branch-diff, etc.)
157
+ * @param {Array} includePatterns - Optional array of glob patterns to include files
158
+ * @param {Array} excludePatterns - Optional array of glob patterns to exclude files
98
159
  */
99
- async buildSecretsApiRequest(type = 'staged-only') {
160
+ async buildSecretsApiRequest(type = 'staged-only', includePatterns = [], excludePatterns = []) {
100
161
  let files;
101
162
 
102
163
  switch (type) {
@@ -116,6 +177,9 @@ class SecretsApiHelper {
116
177
  files = await this.getStagedFilesForApi();
117
178
  }
118
179
 
180
+ // Apply include/exclude filters
181
+ files = this._filterFiles(files, includePatterns, excludePatterns);
182
+
119
183
  return { files };
120
184
  }
121
185