codeant-cli 0.2.2 → 0.2.4

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.2.2",
3
+ "version": "0.2.4",
4
4
  "description": "Code review CLI tool",
5
5
  "type": "module",
6
6
  "bin": {
package/src/index.js CHANGED
@@ -9,8 +9,6 @@ import SetBaseUrl from './commands/setBaseUrl.js';
9
9
  import GetBaseUrl from './commands/getBaseUrl.js';
10
10
  import Login from './commands/login.js';
11
11
  import Logout from './commands/logout.js';
12
- import StaticAnalysis from './commands/staticAnalysis.js';
13
- import SecurityAnalysis from './commands/securityAnalysis.js';
14
12
  import Review from './commands/review.js';
15
13
  import Welcome from './components/Welcome.js';
16
14
  import * as scm from './scm/index.js';
@@ -88,87 +86,6 @@ program
88
86
  render(React.createElement(Secrets, { scanType, failOn, include, exclude, lastNCommits }));
89
87
  });
90
88
 
91
- program
92
- .command('static-analysis')
93
- .description('Run static analysis on your code')
94
- .option('--staged', 'Scan only staged files (default)')
95
- .option('--all', 'Scan all changed files')
96
- .option('--uncommitted', 'Scan uncommitted changes')
97
- .option('--last-commit', 'Scan last commit')
98
- .option('--last-n-commits <n>', 'Scan last n commits (max 5)', parseInt)
99
- .option('--fail-on <level>', 'Fail on issues at or above this level: BLOCKER, CRITICAL, MAJOR, MINOR, INFO (default: CRITICAL)', 'CRITICAL')
100
- .option('--auto-fix', 'Automatically apply fixes when available')
101
- .option('--include <paths>', 'Comma-separated list of file paths regex to include')
102
- .option('--exclude <paths>', 'Comma-separated list of file paths regex to exclude')
103
- .action((options) => {
104
- let scanType = 'staged-only';
105
- let lastNCommits = 1;
106
-
107
- // Check !== undefined, not truthy — passing 0 is valid and shouldn't fall through
108
- if (options.lastNCommits !== undefined) {
109
- scanType = 'last-n-commits';
110
- lastNCommits = Math.min(Math.max(1, options.lastNCommits), 5);
111
- } else if (options.all) {
112
- scanType = 'branch-diff';
113
- } else if (options.uncommitted) {
114
- scanType = 'uncommitted';
115
- } else if (options.lastCommit) {
116
- scanType = 'last-commit';
117
- }
118
-
119
- const include = options.include
120
- ? (Array.isArray(options.include) ? options.include : splitGlobs(options.include))
121
- : [];
122
-
123
- const exclude = options.exclude
124
- ? (Array.isArray(options.exclude) ? options.exclude : splitGlobs(options.exclude))
125
- : [];
126
-
127
- const failOn = options.failOn?.toUpperCase() || 'CRITICAL';
128
- const autoFix = options.autoFix || false;
129
- render(React.createElement(StaticAnalysis, {scanType, failOn, include, exclude, autoFix, lastNCommits}));
130
- });
131
-
132
- program
133
- .command('security-analysis')
134
- .description('Run security analysis on your code')
135
- .option('--staged', 'Scan only staged files (default)')
136
- .option('--all', 'Scan all changed files')
137
- .option('--uncommitted', 'Scan uncommitted changes')
138
- .option('--last-commit', 'Scan last commit')
139
- .option('--last-n-commits <n>', 'Scan last n commits (max 5)', parseInt)
140
- .option('--fail-on <level>', 'Fail on issues at or above this level: BLOCKER, CRITICAL, MAJOR, MINOR, INFO (default: CRITICAL)', 'CRITICAL')
141
- .option('--include <paths>', 'Comma-separated list of file paths regex to include')
142
- .option('--exclude <paths>', 'Comma-separated list of file paths regex to exclude')
143
- .action((options) => {
144
- let scanType = 'staged-only';
145
- let lastNCommits = 1;
146
-
147
- // Check !== undefined, not truthy — passing 0 is valid and shouldn't fall through
148
- if (options.lastNCommits !== undefined) {
149
- scanType = 'last-n-commits';
150
- lastNCommits = Math.min(Math.max(1, options.lastNCommits), 5);
151
- } else if (options.all) {
152
- scanType = 'branch-diff';
153
- } else if (options.uncommitted) {
154
- scanType = 'uncommitted';
155
- } else if (options.lastCommit) {
156
- scanType = 'last-commit';
157
- }
158
-
159
- const include = options.include
160
- ? (Array.isArray(options.include) ? options.include : splitGlobs(options.include))
161
- : [];
162
-
163
- const exclude = options.exclude
164
- ? (Array.isArray(options.exclude) ? options.exclude : splitGlobs(options.exclude))
165
- : [];
166
-
167
- const failOn = options.failOn?.toUpperCase() || 'HIGH';
168
-
169
- render(React.createElement(SecurityAnalysis, { scanType, failOn, include, exclude, lastNCommits }));
170
- });
171
-
172
89
  program
173
90
  .command('review')
174
91
  .description('Run AI-powered code review')
@@ -21,7 +21,7 @@ export async function listPullRequestComments({ name, prNumber, codeantGenerated
21
21
  line: thread.threadContext?.rightFileStart?.line || null,
22
22
  createdAt: comment.publishedDate,
23
23
  updatedAt: comment.lastUpdatedDate,
24
- isCodeantComment: (comment.author?.displayName || '').toLowerCase().includes('codeant'),
24
+ isCodeantComment: (comment.author?.displayName || '').toLowerCase().includes('codeant') || comment.content?.includes('Suggestion'),
25
25
  resolved: thread.status === 2, // Fixed
26
26
  });
27
27
  }
@@ -15,7 +15,7 @@ export async function listPullRequestComments({ name, prNumber, codeantGenerated
15
15
  line: c.inline?.to || null,
16
16
  createdAt: c.created_on,
17
17
  updatedAt: c.updated_on,
18
- isCodeantComment: (c.user?.display_name || c.user?.nickname || '').toLowerCase().includes('codeant'),
18
+ isCodeantComment: (c.user?.display_name || c.user?.nickname || '').toLowerCase().includes('codeant') || (c.content?.raw || c.content?.markup || '').includes('Suggestion'),
19
19
  resolved: c.resolved || false,
20
20
  }));
21
21
 
@@ -20,7 +20,7 @@ export async function listPullRequestComments({ name, prNumber, codeantGenerated
20
20
  line: c.line || c.original_line,
21
21
  createdAt: c.created_at,
22
22
  updatedAt: c.updated_at,
23
- isCodeantComment: c.user?.login?.includes('codeant'),
23
+ isCodeantComment: c.user?.login?.includes('codeant') || c.body?.includes('Suggestion'),
24
24
  inReplyToId: c.in_reply_to_id || null,
25
25
  })),
26
26
  ...issueComments.map(c => ({
@@ -32,7 +32,7 @@ export async function listPullRequestComments({ name, prNumber, codeantGenerated
32
32
  line: null,
33
33
  createdAt: c.created_at,
34
34
  updatedAt: c.updated_at,
35
- isCodeantComment: c.user?.login?.includes('codeant'),
35
+ isCodeantComment: c.user?.login?.includes('codeant') || c.body?.includes('Suggestion'),
36
36
  inReplyToId: null,
37
37
  })),
38
38
  ];
@@ -17,7 +17,7 @@ export async function listPullRequestComments({ name, prNumber, codeantGenerated
17
17
  line: n.position?.new_line || null,
18
18
  createdAt: n.created_at,
19
19
  updatedAt: n.updated_at,
20
- isCodeantComment: n.author?.username?.includes('codeant'),
20
+ isCodeantComment: n.author?.username?.includes('codeant') || n.body?.includes('Suggestion'),
21
21
  resolved: n.resolved || false,
22
22
  discussionId: null,
23
23
  }));
@@ -1,284 +0,0 @@
1
- import React, { useEffect, useState } from 'react';
2
- import { Box, Text, useApp } from 'ink';
3
- import { getConfigValue } from '../utils/config.js';
4
- import { fetchApi } from '../utils/fetchApi.js';
5
- import SecurityAnalysisApiHelper from '../utils/securityAnalysisApiHelper.js';
6
-
7
- export default function SecurityAnalysis({ scanType = 'staged-only', failOn = 'HIGH', include = [], exclude = [], lastNCommits = 1 }) {
8
- const { exit } = useApp();
9
- const [status, setStatus] = useState('initializing');
10
- const [issues, setIssues] = useState([]);
11
- const [error, setError] = useState(null);
12
- const [fileCount, setFileCount] = useState(0);
13
-
14
- const apiKey = getConfigValue('apiKey');
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
-
24
- // Check if logged in
25
- if (!apiKey) {
26
- return React.createElement(
27
- Box,
28
- { flexDirection: 'column', padding: 1 },
29
- React.createElement(Text, { color: 'red' }, '✗ Not logged in.'),
30
- React.createElement(Text, { color: 'gray' }, 'Run "codeant login" to authenticate.'),
31
- );
32
- }
33
-
34
- // Helper to check if an issue should cause failure based on failOn level
35
- const shouldFailOn = (severity) => {
36
- const score = severity?.toUpperCase();
37
- if (score === 'FALSE_POSITIVE') return false;
38
- if (failOn === 'CRITICAL') return score === 'CRITICAL';
39
- if (failOn === 'HIGH') return score === 'HIGH' || score === 'CRITICAL';
40
- if (failOn === 'MEDIUM') return score === 'HIGH' || score === 'MEDIUM';
41
- return true; // 'all' - fail on any non-false-positive
42
- };
43
-
44
- useEffect(() => {
45
- let cancelled = false;
46
-
47
- async function scanForIssues() {
48
- try {
49
- if (cancelled) return;
50
- setStatus('scanning');
51
-
52
- // Initialize git helper and get staged files
53
- const helper = new SecurityAnalysisApiHelper(process.cwd());
54
- await helper.init();
55
-
56
- if (cancelled) return;
57
- const requestBody = await helper.buildSecurityAnalysisApiRequest(scanType, include, exclude, { lastNCommits });
58
- if (cancelled) return;
59
-
60
- if (requestBody.files.length === 0) {
61
- if (!cancelled) setStatus('no_files');
62
- return;
63
- }
64
-
65
- // Call the security analysis API
66
- const response = await fetchApi(
67
- '/extension/pr-review/security-analysis',
68
- 'POST',
69
- requestBody,
70
- );
71
-
72
- if (cancelled) return;
73
-
74
- // Transform security issues response to the expected format
75
- const detectedIssues = (response.securityIssues || []).map(file => ({
76
- file_path: file.file_path,
77
- issues: (file.security_issues || []).map(issue => ({
78
- line_number: issue.line_number,
79
- end_line: issue.end_line,
80
- type: issue.issue_text,
81
- confidence_score: issue.false_positive ? 'FALSE_POSITIVE' : issue.severity,
82
- test_id: issue.test_id,
83
- confidence: issue.confidence,
84
- likelihood: issue.likelihood,
85
- cwe: issue.cwe,
86
- owasp: issue.owasp,
87
- })),
88
- }));
89
-
90
- // Filter to only include files with actual issues
91
- const filesWithIssues = detectedIssues.filter(
92
- file => file.issues && file.issues.length > 0,
93
- );
94
-
95
- if (!cancelled) {
96
- setIssues(filesWithIssues);
97
- setStatus('done');
98
- }
99
- } catch (err) {
100
- if (!cancelled) {
101
- setError(err.message);
102
- setStatus('error');
103
- }
104
- }
105
- }
106
-
107
- scanForIssues();
108
-
109
- return () => {
110
- cancelled = true;
111
- };
112
- }, [scanType, include, exclude, lastNCommits]);
113
-
114
- // Handle exit after status changes
115
- useEffect(() => {
116
- if (status === 'done') {
117
- // Check if any issues should cause failure based on failOn level
118
- const hasBlockingIssues = issues.some(file =>
119
- file.issues.some(issue => issue.false_positive !== true &&shouldFailOn(issue.severity)),
120
- );
121
-
122
- if (hasBlockingIssues) {
123
- setTimeout(() => {
124
- process.exitCode = 1;
125
- exit(new Error('Security issues detected'));
126
- }, 100);
127
- } else {
128
- setTimeout(() => exit(), 100);
129
- }
130
- } else if (status === 'no_files') {
131
- setTimeout(() => exit(), 100);
132
- } else if (status === 'error') {
133
- setTimeout(() => exit(new Error(error)), 100);
134
- }
135
- }, [status, issues]);
136
-
137
- // Render: Initializing
138
- if (status === 'initializing') {
139
- return React.createElement(
140
- Box,
141
- { flexDirection: 'column', padding: 1 },
142
- React.createElement(Text, { color: 'cyan' }, 'Initializing...'),
143
- );
144
- }
145
-
146
- // Render: Scanning
147
- if (status === 'scanning') {
148
- return React.createElement(
149
- Box,
150
- { flexDirection: 'column', padding: 1 },
151
- React.createElement(Text, { color: 'cyan' }, `Scanning ${fileCount} file(s) for security issues...`),
152
- );
153
- }
154
-
155
- // Render: No files to scan
156
- if (status === 'no_files') {
157
- const noFilesMessages = {
158
- 'staged-only': 'Stage some changes first with "git add".',
159
- 'branch-diff': 'No changes found compared to the base branch.',
160
- 'uncommitted': 'No uncommitted changes found.',
161
- 'last-commit': 'No files found in the last commit.',
162
- };
163
-
164
- return React.createElement(
165
- Box,
166
- { flexDirection: 'column', padding: 1 },
167
- React.createElement(Text, { color: 'yellow' }, 'No files to scan.'),
168
- React.createElement(Text, { color: 'gray' }, noFilesMessages[scanType] || 'No files found.'),
169
- );
170
- }
171
-
172
- // Render: Error
173
- if (status === 'error') {
174
- return React.createElement(
175
- Box,
176
- { flexDirection: 'column', padding: 1 },
177
- React.createElement(Text, { color: 'red' }, '✗ Error: ', error),
178
- );
179
- }
180
-
181
- // Render: Done with issues found
182
- if (status === 'done' && issues.length > 0) {
183
- const allIssues = issues.flatMap(file =>
184
- file.issues.map(s => ({ ...s, file_path: file.file_path })),
185
- );
186
-
187
- const blockingIssues = allIssues.filter(s => s.false_positive !== true && shouldFailOn(s.severity));
188
- const falsePositives = allIssues.filter(s => s.false_positive !== true && !shouldFailOn(s.severity));
189
-
190
- const hasBlockingIssues = blockingIssues.length > 0;
191
-
192
- // Group by file for display
193
- const groupByFile = (issuesList) => {
194
- const grouped = {};
195
- issuesList.forEach(s => {
196
- if (!grouped[s.file_path]) grouped[s.file_path] = [];
197
- grouped[s.file_path].push(s);
198
- });
199
- return grouped;
200
- };
201
-
202
- const blockingByFile = groupByFile(blockingIssues);
203
- const falsePositivesByFile = groupByFile(falsePositives);
204
-
205
- const elements = [
206
- // Header
207
- React.createElement(
208
- Text,
209
- { key: 'header', color: hasBlockingIssues ? 'red' : 'yellow', bold: true },
210
- hasBlockingIssues
211
- ? `✗ ${blockingIssues.length} security issue(s) found!`
212
- : `⚠ ${falsePositives.length} potential security issue(s) found (ignored)`,
213
- ),
214
- React.createElement(Text, { key: 'spacer1' }, ''),
215
- ];
216
-
217
- // Blocking issues
218
- if (blockingIssues.length > 0) {
219
- Object.entries(blockingByFile).forEach(([filePath, fileIssues]) => {
220
- elements.push(
221
- React.createElement(Text, { key: `file-${filePath}`, color: 'yellow', bold: true }, filePath),
222
- );
223
- fileIssues.forEach((issue, idx) => {
224
- elements.push(
225
- React.createElement(
226
- Text,
227
- { key: `issue-${filePath}-${idx}`, color: 'red' },
228
- ` Line ${issue.line_number}: ${issue.type} (${issue.confidence_score})`,
229
- ),
230
- );
231
- });
232
- });
233
- }
234
-
235
- // False positives / ignored
236
- if (falsePositives.length > 0) {
237
- if (blockingIssues.length > 0) {
238
- elements.push(React.createElement(Text, { key: 'spacer2' }, ''));
239
- }
240
- elements.push(
241
- React.createElement(Text, { key: 'fp-header', color: 'gray' }, 'Ignored (false positives):'),
242
- );
243
- Object.entries(falsePositivesByFile).forEach(([filePath, fileIssues]) => {
244
- elements.push(
245
- React.createElement(Text, { key: `fp-file-${filePath}`, color: 'gray' }, ` ${filePath}`),
246
- );
247
- fileIssues.forEach((issue, idx) => {
248
- elements.push(
249
- React.createElement(
250
- Text,
251
- { key: `fp-issue-${filePath}-${idx}`, color: 'gray', dimColor: true },
252
- ` Line ${issue.line_number}: ${issue.type} (${issue.confidence_score})`,
253
- ),
254
- );
255
- });
256
- });
257
- }
258
-
259
- elements.push(React.createElement(Text, { key: 'spacer3' }, ''));
260
-
261
- if (hasBlockingIssues) {
262
- elements.push(
263
- React.createElement(Text, { key: 'footer', color: 'gray' }, 'Fix security issues before committing.'),
264
- );
265
- } else {
266
- elements.push(
267
- React.createElement(Text, { key: 'footer', color: 'green' }, '✓ Commit allowed (only false positives found)'),
268
- );
269
- }
270
-
271
- return React.createElement(
272
- Box,
273
- { flexDirection: 'column', padding: 1 },
274
- ...elements,
275
- );
276
- }
277
-
278
- // Render: Done with no issues
279
- return React.createElement(
280
- Box,
281
- { flexDirection: 'column', padding: 1 },
282
- React.createElement(Text, { color: 'green' }, '✓ No security issues found'),
283
- );
284
- }