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.
- package/dist/178.index.js +1 -1
- package/dist/239.index.js +1 -1
- package/dist/701.index.js +1 -1
- package/dist/880.index.js +1 -1
- package/dist/935.index.js +1 -1
- package/dist/966.index.js +1 -1
- package/dist/index.js +5 -5
- package/dist/review/239.index.js +1 -0
- package/dist/review/index.js +8 -0
- package/dist/review/package.json +3 -0
- package/package.json +5 -6
- package/src/commands/getBaseUrl.js +0 -21
- package/src/commands/login.js +0 -125
- package/src/commands/logout.js +0 -30
- package/src/commands/review.js +0 -294
- package/src/commands/secrets.js +0 -148
- package/src/commands/setBaseUrl.js +0 -37
- package/src/components/ReviewUI.js +0 -410
- package/src/components/SecretsUI.js +0 -366
- package/src/components/Welcome.js +0 -144
- package/src/index.js +0 -349
- package/src/reviewHeadless.js +0 -198
- package/src/scm/azure/client.js +0 -21
- package/src/scm/azure/getCodeReview.js +0 -20
- package/src/scm/azure/getPullRequest.js +0 -30
- package/src/scm/azure/index.js +0 -6
- package/src/scm/azure/listCodeReviews.js +0 -17
- package/src/scm/azure/listPullRequestComments.js +0 -41
- package/src/scm/azure/listPullRequests.js +0 -26
- package/src/scm/azure/searchComments.js +0 -35
- package/src/scm/bitbucket/client.js +0 -18
- package/src/scm/bitbucket/getCodeReview.js +0 -3
- package/src/scm/bitbucket/getPullRequest.js +0 -26
- package/src/scm/bitbucket/index.js +0 -6
- package/src/scm/bitbucket/listCodeReviews.js +0 -17
- package/src/scm/bitbucket/listPullRequestComments.js +0 -33
- package/src/scm/bitbucket/listPullRequests.js +0 -33
- package/src/scm/bitbucket/searchComments.js +0 -29
- package/src/scm/github/client.js +0 -17
- package/src/scm/github/getCodeReview.js +0 -26
- package/src/scm/github/getPullRequest.js +0 -27
- package/src/scm/github/index.js +0 -6
- package/src/scm/github/listCodeReviews.js +0 -17
- package/src/scm/github/listPullRequestComments.js +0 -51
- package/src/scm/github/listPullRequests.js +0 -32
- package/src/scm/github/searchComments.js +0 -30
- package/src/scm/gitlab/client.js +0 -16
- package/src/scm/gitlab/getCodeReview.js +0 -18
- package/src/scm/gitlab/getPullRequest.js +0 -24
- package/src/scm/gitlab/index.js +0 -6
- package/src/scm/gitlab/listCodeReviews.js +0 -21
- package/src/scm/gitlab/listPullRequestComments.js +0 -44
- package/src/scm/gitlab/listPullRequests.js +0 -26
- package/src/scm/gitlab/searchComments.js +0 -28
- package/src/scm/index.js +0 -96
- package/src/tools/executeTool.js +0 -31
- package/src/tools/globTool.js +0 -9
- package/src/tools/grepTool.js +0 -18
- package/src/tools/lsTool.js +0 -7
- package/src/tools/readTool.js +0 -12
- package/src/utils/analytics.js +0 -45
- package/src/utils/baseUrl.js +0 -11
- package/src/utils/commonApiHelper.js +0 -143
- package/src/utils/config.js +0 -42
- package/src/utils/fetchApi.js +0 -39
- package/src/utils/gitDiffHelper.js +0 -1203
- package/src/utils/reviewApiHelper.js +0 -275
- package/src/utils/scmAuth.js +0 -136
- package/src/utils/secretsApiHelper.js +0 -242
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "codeant-cli",
|
|
3
|
-
"version": "0.3.
|
|
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": "./
|
|
26
|
+
"main": "./dist/review/index.js",
|
|
27
27
|
"exports": {
|
|
28
|
-
".": "./
|
|
29
|
-
"./review": "./
|
|
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
|
-
}
|
package/src/commands/login.js
DELETED
|
@@ -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
|
-
}
|
package/src/commands/logout.js
DELETED
|
@@ -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
|
-
}
|
package/src/commands/review.js
DELETED
|
@@ -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
|
-
|
package/src/commands/secrets.js
DELETED
|
@@ -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
|
-
}
|