codeant-cli 0.3.1 → 0.3.3

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.
Files changed (69) hide show
  1. package/dist/178.index.js +1 -1
  2. package/dist/239.index.js +1 -1
  3. package/dist/701.index.js +1 -1
  4. package/dist/880.index.js +1 -1
  5. package/dist/935.index.js +1 -1
  6. package/dist/966.index.js +1 -1
  7. package/dist/index.js +5 -5
  8. package/dist/review/239.index.js +1 -0
  9. package/dist/review/index.js +8 -0
  10. package/dist/review/package.json +3 -0
  11. package/package.json +5 -6
  12. package/src/commands/getBaseUrl.js +0 -21
  13. package/src/commands/login.js +0 -125
  14. package/src/commands/logout.js +0 -30
  15. package/src/commands/review.js +0 -294
  16. package/src/commands/secrets.js +0 -148
  17. package/src/commands/setBaseUrl.js +0 -37
  18. package/src/components/ReviewUI.js +0 -410
  19. package/src/components/SecretsUI.js +0 -366
  20. package/src/components/Welcome.js +0 -144
  21. package/src/index.js +0 -349
  22. package/src/reviewHeadless.js +0 -198
  23. package/src/scm/azure/client.js +0 -21
  24. package/src/scm/azure/getCodeReview.js +0 -20
  25. package/src/scm/azure/getPullRequest.js +0 -30
  26. package/src/scm/azure/index.js +0 -6
  27. package/src/scm/azure/listCodeReviews.js +0 -17
  28. package/src/scm/azure/listPullRequestComments.js +0 -41
  29. package/src/scm/azure/listPullRequests.js +0 -26
  30. package/src/scm/azure/searchComments.js +0 -35
  31. package/src/scm/bitbucket/client.js +0 -18
  32. package/src/scm/bitbucket/getCodeReview.js +0 -3
  33. package/src/scm/bitbucket/getPullRequest.js +0 -26
  34. package/src/scm/bitbucket/index.js +0 -6
  35. package/src/scm/bitbucket/listCodeReviews.js +0 -17
  36. package/src/scm/bitbucket/listPullRequestComments.js +0 -33
  37. package/src/scm/bitbucket/listPullRequests.js +0 -33
  38. package/src/scm/bitbucket/searchComments.js +0 -29
  39. package/src/scm/github/client.js +0 -17
  40. package/src/scm/github/getCodeReview.js +0 -26
  41. package/src/scm/github/getPullRequest.js +0 -27
  42. package/src/scm/github/index.js +0 -6
  43. package/src/scm/github/listCodeReviews.js +0 -17
  44. package/src/scm/github/listPullRequestComments.js +0 -51
  45. package/src/scm/github/listPullRequests.js +0 -32
  46. package/src/scm/github/searchComments.js +0 -30
  47. package/src/scm/gitlab/client.js +0 -16
  48. package/src/scm/gitlab/getCodeReview.js +0 -18
  49. package/src/scm/gitlab/getPullRequest.js +0 -24
  50. package/src/scm/gitlab/index.js +0 -6
  51. package/src/scm/gitlab/listCodeReviews.js +0 -21
  52. package/src/scm/gitlab/listPullRequestComments.js +0 -44
  53. package/src/scm/gitlab/listPullRequests.js +0 -26
  54. package/src/scm/gitlab/searchComments.js +0 -28
  55. package/src/scm/index.js +0 -96
  56. package/src/tools/executeTool.js +0 -31
  57. package/src/tools/globTool.js +0 -9
  58. package/src/tools/grepTool.js +0 -18
  59. package/src/tools/lsTool.js +0 -7
  60. package/src/tools/readTool.js +0 -12
  61. package/src/utils/analytics.js +0 -45
  62. package/src/utils/baseUrl.js +0 -11
  63. package/src/utils/commonApiHelper.js +0 -143
  64. package/src/utils/config.js +0 -42
  65. package/src/utils/fetchApi.js +0 -39
  66. package/src/utils/gitDiffHelper.js +0 -1203
  67. package/src/utils/reviewApiHelper.js +0 -275
  68. package/src/utils/scmAuth.js +0 -136
  69. package/src/utils/secretsApiHelper.js +0 -242
@@ -0,0 +1,3 @@
1
+ {
2
+ "type": "module"
3
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codeant-cli",
3
- "version": "0.3.1",
3
+ "version": "0.3.3",
4
4
  "description": "Code review CLI tool",
5
5
  "type": "module",
6
6
  "bin": {
@@ -23,14 +23,13 @@
23
23
  "type": "git",
24
24
  "url": ""
25
25
  },
26
- "main": "./src/reviewHeadless.js",
26
+ "main": "./dist/review/index.js",
27
27
  "exports": {
28
- ".": "./src/reviewHeadless.js",
29
- "./review": "./src/reviewHeadless.js"
28
+ ".": "./dist/review/index.js",
29
+ "./review": "./dist/review/index.js"
30
30
  },
31
31
  "files": [
32
- "dist",
33
- "src"
32
+ "dist"
34
33
  ],
35
34
  "dependencies": {
36
35
  "@gitbeaker/rest": "^43.8.0",
@@ -1,21 +0,0 @@
1
- import React, { useEffect } from 'react';
2
- import { Text, Box, useApp } from 'ink';
3
- import { getBaseUrl } from '../utils/baseUrl.js';
4
-
5
- export default function GetBaseUrl() {
6
- const { exit } = useApp();
7
-
8
- const activeUrl = getBaseUrl();
9
- const source = process.env.CODEANT_API_URL ? 'env' : 'config';
10
-
11
- useEffect(() => {
12
- exit();
13
- }, []);
14
-
15
- return React.createElement(
16
- Box,
17
- { flexDirection: 'column', padding: 1 },
18
- React.createElement(Text, { bold: true }, 'Base URL: ', activeUrl),
19
- React.createElement(Text, { color: 'gray' }, 'Source: ', source)
20
- );
21
- }
@@ -1,125 +0,0 @@
1
- import React, { useState, useEffect } from 'react';
2
- import { Text, Box, useApp } from 'ink';
3
- import { getConfigValue, setConfigValue } from '../utils/config.js';
4
- import { getBaseUrl } from '../utils/baseUrl.js';
5
- import { randomUUID } from 'crypto';
6
-
7
- const POLL_INTERVAL = 10000; // 10 seconds
8
- const TIMEOUT = 10 * 60 * 1000; // 10 minutes
9
-
10
- export default function Login() {
11
- const { exit } = useApp();
12
- const [status, setStatus] = useState('opening');
13
- const [error, setError] = useState(null);
14
-
15
- useEffect(() => {
16
- // Check if already logged in
17
- const existingToken = getConfigValue('apiKey');
18
- if (existingToken) {
19
- setStatus('already_logged_in');
20
- setTimeout(() => exit(), 100);
21
- return;
22
- }
23
-
24
- const token = randomUUID();
25
- const baseUrl = getBaseUrl();
26
- const loginUrl = `https://app.codeant.ai?ideLoginToken=${token}`;
27
- const pollUrl = `${baseUrl}/extension/login/status?apiKey=${token}`;
28
-
29
- // Open browser
30
- import('open').then(({ default: open }) => {
31
- open(loginUrl);
32
- setStatus('waiting');
33
- }).catch(() => {
34
- // Fallback: just show the URL
35
- console.log(`\nOpen this URL in your browser:\n${loginUrl}\n`);
36
- setStatus('waiting');
37
- });
38
-
39
- // Poll for login status
40
- let timeoutId;
41
- const intervalId = setInterval(async () => {
42
- try {
43
- const response = await fetch(pollUrl);
44
- const data = await response.json();
45
-
46
- if (data.status === 'yes') {
47
- clearInterval(intervalId);
48
- clearTimeout(timeoutId);
49
-
50
- // Save the API key
51
- setConfigValue('apiKey', token);
52
-
53
- // Check for custom API URL based on email domain
54
- if (data.item?.email?.includes('commvault.com')) {
55
- setConfigValue('baseUrl', 'https://codeant-api.commvault.com');
56
- }
57
-
58
- setStatus('success');
59
- setTimeout(() => exit(), 100);
60
- }
61
- } catch (err) {
62
- // Silently continue polling
63
- }
64
- }, POLL_INTERVAL);
65
-
66
- // Timeout after 10 minutes
67
- timeoutId = setTimeout(() => {
68
- clearInterval(intervalId);
69
- setError('Login timed out. Please try again.');
70
- setStatus('error');
71
- setTimeout(() => exit(new Error('Login timed out')), 100);
72
- }, TIMEOUT);
73
-
74
- // Cleanup
75
- return () => {
76
- clearInterval(intervalId);
77
- clearTimeout(timeoutId);
78
- };
79
- }, []);
80
-
81
- if (status === 'already_logged_in') {
82
- return React.createElement(
83
- Box,
84
- { flexDirection: 'column', padding: 1 },
85
- React.createElement(Text, { color: 'yellow' }, 'Already logged in.'),
86
- React.createElement(Text, { color: 'gray' }, 'Run "codeant logout" first to re-authenticate.')
87
- );
88
- }
89
-
90
- if (status === 'opening') {
91
- return React.createElement(
92
- Box,
93
- { flexDirection: 'column', padding: 1 },
94
- React.createElement(Text, null, 'Opening browser...')
95
- );
96
- }
97
-
98
- if (status === 'waiting') {
99
- return React.createElement(
100
- Box,
101
- { flexDirection: 'column', padding: 1 },
102
- React.createElement(Text, { color: 'cyan' }, 'Waiting for login...'),
103
- React.createElement(Text, { color: 'gray' }, 'Complete the login in your browser.'),
104
- React.createElement(Text, { color: 'gray' }, 'Checking every 10 seconds. Timeout in 10 minutes.')
105
- );
106
- }
107
-
108
- if (status === 'success') {
109
- return React.createElement(
110
- Box,
111
- { flexDirection: 'column', padding: 1 },
112
- React.createElement(Text, { color: 'green' }, '✓ Login successful!')
113
- );
114
- }
115
-
116
- if (status === 'error') {
117
- return React.createElement(
118
- Box,
119
- { flexDirection: 'column', padding: 1 },
120
- React.createElement(Text, { color: 'red' }, '✗ ', error)
121
- );
122
- }
123
-
124
- return null;
125
- }
@@ -1,30 +0,0 @@
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
- }
@@ -1,294 +0,0 @@
1
- import React, { useEffect, useState } from 'react';
2
- import { useApp } from 'ink';
3
- import { getConfigValue } from '../utils/config.js';
4
- import ReviewApiHelper from '../utils/reviewApiHelper.js';
5
- import { fetchApi } from '../utils/fetchApi.js';
6
- import fs from 'fs';
7
- import path from 'path';
8
- import { executeTool } from '../tools/executeTool.js';
9
- import { track, shutdown as analyticsShutdown } from '../utils/analytics.js';
10
- import {
11
- renderInitializing,
12
- renderFetchingDiff,
13
- renderReviewing,
14
- renderNoFiles,
15
- renderError,
16
- renderNotLoggedIn,
17
- renderDone,
18
- mapLabelToSeverity,
19
- } from '../components/ReviewUI.js';
20
-
21
- const MAX_TURNS = 5;
22
-
23
- export default function Review({ scanType = 'all', lastNCommits = 1, failOn = 'CRITICAL', include = [], exclude = [], baseBranch = null, baseCommit = null }) {
24
- const { exit } = useApp();
25
- const [status, setStatus] = useState('initializing');
26
- const [error, setError] = useState(null);
27
- const [currentMessage, setCurrentMessage] = useState('');
28
- const [issues, setIssues] = useState([]);
29
- const [summary, setSummary] = useState(null);
30
- const [startTime] = useState(() => Date.now());
31
- const [fileCount, setFileCount] = useState(0);
32
- const [reviewMeta, setReviewMeta] = useState(null);
33
-
34
- const apiKey = getConfigValue('apiKey');
35
-
36
- useEffect(() => {
37
- if (!apiKey) {
38
- if (process.stdin.isTTY) {
39
- // Interactive session: don't exit immediately, let component render the error
40
- return;
41
- }
42
- const id = setTimeout(() => exit(new Error('Not logged in')), 0);
43
- return () => clearTimeout(id);
44
- }
45
- }, [apiKey, exit]);
46
-
47
- if (!apiKey) {
48
- return renderNotLoggedIn();
49
- }
50
-
51
- useEffect(() => {
52
- let cancelled = false;
53
-
54
- async function runReview() {
55
- const reviewStartTime = Date.now();
56
- track('review_triggered', { scan_type: scanType, source: 'cli_interactive' });
57
-
58
- try {
59
- // ── Fetch diff ─────────────────────────────────────────────────────
60
- setStatus('fetching_diff');
61
-
62
- const helper = new ReviewApiHelper(process.cwd());
63
- await helper.init();
64
- const gitRoot = helper.getGitRoot() || process.cwd();
65
- const requestBody = await helper.buildReviewApiRequest(scanType, include, exclude, { lastNCommits, baseBranch, baseCommit });
66
-
67
- const meta = requestBody?._meta || null;
68
- setFileCount(meta?.reviewed_files?.length || Object.keys(requestBody?.file_contents || {}).length);
69
- setReviewMeta(meta);
70
-
71
- // Strip _meta before sending to API
72
- delete requestBody._meta;
73
-
74
- if (!requestBody?.diff_content?.length) {
75
- if (!cancelled) setStatus('no_files');
76
- return;
77
- }
78
-
79
- if (cancelled) return;
80
- setStatus('reviewing');
81
-
82
- // ── Split into per-file requests ───────────────────────────────────
83
- const perFileRequests = ReviewApiHelper.splitIntoPerFileRequests(requestBody);
84
-
85
- // ── Per-file agent turn loops (parallel) ──────────────────────────
86
- setCurrentMessage(`Analyzing ${perFileRequests.length} file${perFileRequests.length !== 1 ? 's' : ''} in parallel...`);
87
-
88
- const perFileResults = await Promise.all(
89
- perFileRequests.map((fileReq) => {
90
- const filename = fileReq._filename;
91
- delete fileReq._filename;
92
-
93
- // Convert file_contents dict → file_content/file_path for API
94
- if (fileReq.file_contents?.[filename]) {
95
- fileReq.file_content = fileReq.file_contents[filename];
96
- fileReq.file_path = filename;
97
- }
98
- delete fileReq.file_contents;
99
-
100
- return runTurnLoop(
101
- fileReq,
102
- gitRoot,
103
- () => cancelled,
104
- () => {},
105
- false
106
- );
107
- })
108
- );
109
-
110
- // Pair each file's suggestions with its own diff — skip files with no real suggestions
111
- const perFileWithSuggestions = perFileRequests.map((fileReq, i) => ({
112
- diff_content: fileReq.diff_content,
113
- suggestions: perFileResults[i].finalMessage?.content,
114
- output: perFileResults[i].finalOutput,
115
- })).filter(r => r.output?.code_suggestions?.length > 0);
116
-
117
- if (cancelled) return;
118
-
119
- // ── Per-file reflector loops (parallel) ─────────────────────────────
120
- setCurrentMessage('Running reflector...');
121
- const reflectorResults = await Promise.all(
122
- perFileWithSuggestions.map(({ diff_content, suggestions }) =>
123
- runTurnLoop(
124
- {
125
- diff_content,
126
- prompt_template_name: 'reflector',
127
- extra_variables: { suggestion_str: suggestions },
128
- },
129
- gitRoot,
130
- () => cancelled,
131
- () => {},
132
- true
133
- )
134
- )
135
- );
136
-
137
- if (cancelled) return;
138
-
139
- const reviewIssues = reflectorResults.flatMap(r =>
140
- (r.finalOutput?.code_suggestions || []).map(issue => ({
141
- issue_content: issue.issue_content || '',
142
- relevant_file: issue.relevant_file || 'Unknown',
143
- start_line: issue.start_line || 0,
144
- label: issue.label || 'Code Quality',
145
- }))
146
- );
147
- setIssues(reviewIssues);
148
- setSummary(null);
149
- setStatus('done');
150
-
151
- const labelCounts = {};
152
- for (const i of reviewIssues) { labelCounts[i.label] = (labelCounts[i.label] || 0) + 1; }
153
- track('review_completed', {
154
- scan_type: scanType,
155
- source: 'cli_interactive',
156
- issue_count: reviewIssues.length,
157
- file_count: perFileRequests.length,
158
- duration_ms: Date.now() - reviewStartTime,
159
- label_counts: labelCounts,
160
- });
161
- await analyticsShutdown();
162
-
163
- } catch (err) {
164
- if (!cancelled) {
165
- const errMsg = err instanceof Error ? err.message : String(err);
166
- track('review_error', { scan_type: scanType, source: 'cli_interactive', error: errMsg });
167
- await analyticsShutdown();
168
- setError(errMsg);
169
- setStatus('error');
170
- }
171
- }
172
- }
173
-
174
- runReview();
175
- return () => { cancelled = true; };
176
- }, [scanType, lastNCommits, apiKey]);
177
-
178
- // Handle exit on terminal states
179
- useEffect(() => {
180
- if (status === 'done') {
181
- const severityOrder = ['INFO', 'MINOR', 'MAJOR', 'CRITICAL', 'BLOCKER'];
182
- const failOnIndex = severityOrder.indexOf(failOn);
183
- if (failOnIndex === -1) {
184
- setTimeout(() => { process.exitCode = 1; exit(new Error(`Invalid failOn value: ${failOn}`)); }, 100);
185
- return;
186
- }
187
- const hasBlocking = issues.some(issue =>
188
- severityOrder.indexOf(mapLabelToSeverity(issue.label)) >= failOnIndex
189
- );
190
- if (hasBlocking) {
191
- setTimeout(() => { process.exitCode = 1; exit(new Error('Issues detected')); }, 100);
192
- } else {
193
- setTimeout(() => exit(), 100);
194
- }
195
- } else if (status === 'no_files') {
196
- setTimeout(() => exit(), 100);
197
- } else if (status === 'error') {
198
- setTimeout(() => exit(new Error(error)), 100);
199
- }
200
- }, [status, issues, failOn, exit, error]);
201
-
202
- // ── Renders ──────────────────────────────────────────────────────────────
203
-
204
- if (status === 'initializing') return renderInitializing(startTime);
205
- if (status === 'fetching_diff') return renderFetchingDiff(startTime);
206
- if (status === 'reviewing') return renderReviewing(currentMessage, startTime, reviewMeta);
207
- if (status === 'no_files') return renderNoFiles(scanType, lastNCommits);
208
- if (status === 'error') return renderError(error);
209
- if (status === 'done') return renderDone(issues, summary, failOn, startTime, fileCount, reviewMeta);
210
-
211
- return null;
212
- }
213
-
214
- // ── Turn loop ──────────────────────────────────────────────────────────────
215
-
216
- async function runTurnLoop(initialPayload, gitRoot, isCancelled, onMessage, isReflectorLoop) {
217
- let nextPayload = initialPayload;
218
- let finalMessage = null;
219
- let finalOutput = null;
220
-
221
- for (let turn = 0; turn < MAX_TURNS; turn+=1) {
222
- if (isCancelled()) break;
223
-
224
- onMessage('Analyzing code...');
225
-
226
- const resp = await fetchApi('/extension/pr-review/agent/turn', 'POST', nextPayload);
227
-
228
- if (!resp || typeof resp !== 'object') {
229
- throw new Error('Invalid response from agent API');
230
- }
231
-
232
- const sessionId = resp.session_id;
233
- const assistantMsg = resp.response;
234
- const done = resp.done;
235
-
236
- if (!sessionId) {
237
- throw new Error('Missing session_id in agent response');
238
- }
239
-
240
- finalMessage = assistantMsg;
241
- if (resp.output) finalOutput = resp.output;
242
-
243
- if (done) {
244
- // Handle parsing errors in final message by dumping response content (only on reflector loop)
245
- if (isReflectorLoop && resp.parsing_error) {
246
- const dumpDir = path.join(gitRoot, '.codeant', 'codeSuggestions');
247
- const dumpFile = path.join(dumpDir, `code_suggestions_${Date.now()}.txt`);
248
- let dumpContent = resp.response?.content || JSON.stringify(resp, null, 2);
249
-
250
- // Ensure dumpContent is a string
251
- if (typeof dumpContent !== 'string') {
252
- dumpContent = JSON.stringify(dumpContent, null, 2);
253
- }
254
-
255
- // Unescape all serialization escape sequences
256
- dumpContent = dumpContent
257
- .replace(/\\x([0-9a-fA-F]{2})/g, (_, hex) => String.fromCharCode(parseInt(hex, 16)))
258
- .replace(/\\u([0-9a-fA-F]{4})/g, (_, hex) => String.fromCharCode(parseInt(hex, 16)))
259
- .replace(/\\n/g, '\n')
260
- .replace(/\\r/g, '\r')
261
- .replace(/\\t/g, '\t')
262
- .replace(/\\"/g, '"')
263
- .replace(/\\'/g, "'")
264
- .replace(/\\\\/g, '\\');
265
-
266
- try {
267
- // Create directories if they don't exist
268
- await fs.promises.mkdir(dumpDir, { recursive: true });
269
- await fs.promises.writeFile(dumpFile, dumpContent, 'utf8');
270
- console.error(`⚠ Parsing error encountered. Full response dumped to: ${dumpFile}`);
271
- } catch (writeErr) {
272
- console.error(`⚠ Parsing error encountered (could not dump to file: ${writeErr.message})`);
273
- }
274
- }
275
- break;
276
- }
277
-
278
- const toolCalls = assistantMsg?.tool_calls || [];
279
- if (toolCalls.length === 0) break;
280
-
281
- // Run tool calls in parallel to avoid serial latency when agent requests multiple reads
282
- const toolResults = await Promise.all(
283
- toolCalls.map(async (tc) => ({
284
- tool_call_id: tc.id,
285
- content: await executeTool(tc, gitRoot),
286
- }))
287
- );
288
-
289
- nextPayload = { session_id: sessionId, tool_results: toolResults };
290
- }
291
-
292
- return { finalMessage, finalOutput };
293
- }
294
-
@@ -1,148 +0,0 @@
1
- import { useState, useEffect } from 'react';
2
- import { 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
- import {
7
- renderInitializing,
8
- renderFetchingDiff,
9
- renderScanning,
10
- renderNoFiles,
11
- renderError,
12
- renderNotLoggedIn,
13
- renderDone,
14
- } from '../components/SecretsUI.js';
15
-
16
- export default function Secrets({ scanType = 'all', failOn = 'CRITICAL', include = [], exclude = [], lastNCommits = 1, baseBranch = null, baseCommit = null }) {
17
- const { exit } = useApp();
18
- const [status, setStatus] = useState('initializing');
19
- const [secrets, setSecrets] = useState([]);
20
- const [error, setError] = useState(null);
21
- const [fileCount, setFileCount] = useState(0);
22
- const [scanMeta, setScanMeta] = useState(null);
23
- const [startTime] = useState(() => Date.now());
24
-
25
- const apiKey = getConfigValue('apiKey');
26
-
27
- // Handle not logged in state
28
- useEffect(() => {
29
- if (!apiKey) {
30
- if (process.stdin.isTTY) {
31
- return;
32
- }
33
- const id = setTimeout(() => exit(new Error('Not logged in')), 0);
34
- return () => clearTimeout(id);
35
- }
36
- }, [apiKey, exit]);
37
-
38
- if (!apiKey) {
39
- return renderNotLoggedIn();
40
- }
41
-
42
- // Helper to check if a secret should cause failure based on failOn level
43
- const shouldFailOn = (confidenceScore) => {
44
- const score = confidenceScore?.toUpperCase();
45
- if (score === 'FALSE_POSITIVE') return false;
46
- if (failOn === 'HIGH') return score === 'HIGH';
47
- if (failOn === 'MEDIUM') return score === 'HIGH' || score === 'MEDIUM';
48
- return true; // 'all' - fail on any non-false-positive
49
- };
50
-
51
- useEffect(() => {
52
- let cancelled = false;
53
-
54
- async function scanSecrets() {
55
- try {
56
- if (cancelled) return;
57
- setStatus('fetching_diff');
58
-
59
- // Initialize git helper and get files
60
- const helper = new SecretsApiHelper(process.cwd());
61
- await helper.init();
62
-
63
- if (cancelled) return;
64
- const requestBody = await helper.buildSecretsApiRequest(scanType, include, exclude, { lastNCommits, baseBranch, baseCommit });
65
- if (cancelled) return;
66
-
67
- const meta = requestBody._meta || null;
68
- setScanMeta(meta);
69
-
70
- // Strip _meta before sending to API
71
- delete requestBody._meta;
72
-
73
- if (requestBody.files.length === 0) {
74
- if (!cancelled) setStatus('no_files');
75
- return;
76
- }
77
-
78
- setFileCount(requestBody.files.length);
79
- setStatus('scanning');
80
-
81
- // Call the secrets detection API
82
- const response = await fetchApi(
83
- '/extension/pr-review/secrets-detection',
84
- 'POST',
85
- requestBody
86
- );
87
-
88
- if (cancelled) return;
89
- const detectedSecrets = response.secretsDetected || [];
90
-
91
- // Filter to only include files with actual secrets
92
- const filesWithSecrets = detectedSecrets.filter(
93
- file => file.secrets && file.secrets.length > 0
94
- );
95
-
96
- if (!cancelled) {
97
- setSecrets(filesWithSecrets);
98
- setStatus('done');
99
- }
100
- } catch (err) {
101
- if (!cancelled) {
102
- setError(err.message);
103
- setStatus('error');
104
- }
105
- }
106
- }
107
-
108
- scanSecrets();
109
-
110
- return () => {
111
- cancelled = true;
112
- };
113
- }, [scanType, include, exclude, lastNCommits, baseBranch, baseCommit]);
114
-
115
- // Handle exit after status changes
116
- useEffect(() => {
117
- if (status === 'done') {
118
- // Check if any secrets should cause failure based on failOn level
119
- const hasBlockingSecrets = secrets.some(file =>
120
- file.secrets.some(secret => shouldFailOn(secret.confidence_score))
121
- );
122
-
123
- if (hasBlockingSecrets) {
124
- setTimeout(() => {
125
- process.exitCode = 1;
126
- exit(new Error('Secrets detected'));
127
- }, 100);
128
- } else {
129
- setTimeout(() => exit(), 100);
130
- }
131
- } else if (status === 'no_files') {
132
- setTimeout(() => exit(), 100);
133
- } else if (status === 'error') {
134
- setTimeout(() => exit(new Error(error)), 100);
135
- }
136
- }, [status, secrets]);
137
-
138
- // ── Renders ──────────────────────────────────────────────────────────────
139
-
140
- if (status === 'initializing') return renderInitializing(startTime);
141
- if (status === 'fetching_diff') return renderFetchingDiff(startTime);
142
- if (status === 'scanning') return renderScanning(startTime, fileCount, scanMeta);
143
- if (status === 'no_files') return renderNoFiles(scanType, lastNCommits, baseBranch, baseCommit);
144
- if (status === 'error') return renderError(error);
145
- if (status === 'done') return renderDone(secrets, failOn, shouldFailOn, startTime, fileCount, scanMeta);
146
-
147
- return null;
148
- }
@@ -1,37 +0,0 @@
1
- import React, { useEffect } from 'react';
2
- import { Text, Box, useApp } from 'ink';
3
- import { setConfigValue, CONFIG_FILE } from '../utils/config.js';
4
-
5
- export default function SetBaseUrl({ url }) {
6
- const { exit } = useApp();
7
-
8
- useEffect(() => {
9
- if (!url) {
10
- exit(new Error('URL is required'));
11
- return;
12
- }
13
-
14
- try {
15
- setConfigValue('baseUrl', url);
16
- exit();
17
- } catch (err) {
18
- exit(err);
19
- }
20
- }, []);
21
-
22
- if (!url) {
23
- return React.createElement(
24
- Box,
25
- { flexDirection: 'column', padding: 1 },
26
- React.createElement(Text, { color: 'red' }, '✗ Error: URL is required'),
27
- React.createElement(Text, { color: 'gray' }, 'Usage: codeant set-base-url <url>')
28
- );
29
- }
30
-
31
- return React.createElement(
32
- Box,
33
- { flexDirection: 'column', padding: 1 },
34
- React.createElement(Text, { color: 'green' }, '✓ Base URL set to: ', url),
35
- React.createElement(Text, { color: 'gray' }, 'Saved to: ', CONFIG_FILE)
36
- );
37
- }