codeant-cli 0.3.1 → 0.3.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/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/package.json +1 -1
- package/src/index.js +19 -0
- package/src/scm/azure/index.js +1 -0
- package/src/scm/azure/resolveConversation.js +22 -0
- package/src/scm/bitbucket/index.js +1 -0
- package/src/scm/bitbucket/resolveConversation.js +21 -0
- package/src/scm/github/index.js +1 -0
- package/src/scm/github/resolveConversation.js +85 -0
- package/src/scm/gitlab/index.js +1 -0
- package/src/scm/gitlab/resolveConversation.js +14 -0
- package/src/scm/index.js +1 -0
- package/src/utils/reviewApiHelper.js +17 -1
- package/src/utils/secretsApiHelper.js +11 -1
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -269,6 +269,25 @@ program
|
|
|
269
269
|
}));
|
|
270
270
|
});
|
|
271
271
|
|
|
272
|
+
pr.command('resolve')
|
|
273
|
+
.description('Resolve a conversation/comment thread on a PR')
|
|
274
|
+
.option('--name <repo>', 'Repository (owner/repo)')
|
|
275
|
+
.option('--remote <provider>', 'github, gitlab, bitbucket, azure')
|
|
276
|
+
.requiredOption('--pr-number <n>', 'PR number', parseInt)
|
|
277
|
+
.option('--comment-id <id>', 'Comment ID (GitHub, Bitbucket)', parseInt)
|
|
278
|
+
.option('--thread-id <id>', 'Thread/node ID (GitHub GraphQL, Azure)')
|
|
279
|
+
.option('--discussion-id <id>', 'Discussion ID (GitLab)')
|
|
280
|
+
.action((options) => {
|
|
281
|
+
const opts = resolveRepoOpts(options);
|
|
282
|
+
runCmd(() => scm.resolveConversation({
|
|
283
|
+
name: opts.name, remote: opts.remote,
|
|
284
|
+
prNumber: opts.prNumber,
|
|
285
|
+
commentId: opts.commentId,
|
|
286
|
+
threadId: opts.threadId,
|
|
287
|
+
discussionId: opts.discussionId,
|
|
288
|
+
}));
|
|
289
|
+
});
|
|
290
|
+
|
|
272
291
|
// ─── Code review commands ───
|
|
273
292
|
const codeReview = program.command('code-review').description('Code review tools');
|
|
274
293
|
|
package/src/scm/azure/index.js
CHANGED
|
@@ -4,3 +4,4 @@ export { listPullRequestComments } from './listPullRequestComments.js';
|
|
|
4
4
|
export { listCodeReviews } from './listCodeReviews.js';
|
|
5
5
|
export { getCodeReview } from './getCodeReview.js';
|
|
6
6
|
export { searchComments } from './searchComments.js';
|
|
7
|
+
export { resolveConversation } from './resolveConversation.js';
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { getConnection, splitRepo } from './client.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Resolve a comment thread on an Azure DevOps pull request.
|
|
5
|
+
* Azure uses thread status: 1 = Active, 2 = Fixed (resolved), 3 = WontFix, 4 = Closed, 5 = ByDesign, 6 = Pending
|
|
6
|
+
*/
|
|
7
|
+
export async function resolveConversation({ name, prNumber, threadId }) {
|
|
8
|
+
if (!threadId) throw new Error('threadId is required to resolve an Azure DevOps thread');
|
|
9
|
+
|
|
10
|
+
const parsedThreadId = Number(threadId);
|
|
11
|
+
if (!Number.isInteger(parsedThreadId) || parsedThreadId <= 0) {
|
|
12
|
+
throw new Error('threadId must be a positive integer for Azure DevOps');
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const connection = getConnection();
|
|
16
|
+
const gitApi = await connection.getGitApi();
|
|
17
|
+
const { project, repo } = splitRepo(name);
|
|
18
|
+
|
|
19
|
+
await gitApi.updateThread({ status: 2 }, repo, prNumber, parsedThreadId, project);
|
|
20
|
+
|
|
21
|
+
return { success: true };
|
|
22
|
+
}
|
|
@@ -4,3 +4,4 @@ export { listPullRequestComments } from './listPullRequestComments.js';
|
|
|
4
4
|
export { listCodeReviews } from './listCodeReviews.js';
|
|
5
5
|
export { getCodeReview } from './getCodeReview.js';
|
|
6
6
|
export { searchComments } from './searchComments.js';
|
|
7
|
+
export { resolveConversation } from './resolveConversation.js';
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { getClient, splitRepo } from './client.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Resolve a comment (conversation) on a Bitbucket pull request.
|
|
5
|
+
*/
|
|
6
|
+
export async function resolveConversation({ name, prNumber, commentId }) {
|
|
7
|
+
if (!commentId) throw new Error('commentId is required to resolve a Bitbucket comment');
|
|
8
|
+
|
|
9
|
+
const client = getClient();
|
|
10
|
+
const { workspace, repo_slug } = splitRepo(name);
|
|
11
|
+
|
|
12
|
+
await client.repositories.updatePullRequestComment({
|
|
13
|
+
workspace,
|
|
14
|
+
repo_slug,
|
|
15
|
+
pull_request_id: prNumber,
|
|
16
|
+
comment_id: commentId,
|
|
17
|
+
_body: { resolved: true },
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
return { success: true };
|
|
21
|
+
}
|
package/src/scm/github/index.js
CHANGED
|
@@ -4,3 +4,4 @@ export { listPullRequestComments } from './listPullRequestComments.js';
|
|
|
4
4
|
export { listCodeReviews } from './listCodeReviews.js';
|
|
5
5
|
export { getCodeReview } from './getCodeReview.js';
|
|
6
6
|
export { searchComments } from './searchComments.js';
|
|
7
|
+
export { resolveConversation } from './resolveConversation.js';
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { getClient, splitRepo } from './client.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Resolve a review conversation on GitHub.
|
|
5
|
+
* GitHub doesn't have a native "resolve conversation" API for individual comments.
|
|
6
|
+
* We use the GraphQL API to minimize (hide) the comment, which is the closest equivalent.
|
|
7
|
+
* For review comments that are part of a PR review thread, we resolve via GraphQL resolveReviewThread.
|
|
8
|
+
*/
|
|
9
|
+
export async function resolveConversation({ name, prNumber, commentId, threadId }) {
|
|
10
|
+
const octokit = getClient();
|
|
11
|
+
const { owner, repo } = splitRepo(name);
|
|
12
|
+
|
|
13
|
+
// If a threadId (graphql node_id of the review thread) is provided, resolve the thread
|
|
14
|
+
if (threadId) {
|
|
15
|
+
await octokit.graphql(
|
|
16
|
+
`mutation($threadId: ID!) {
|
|
17
|
+
resolveReviewThread(input: { threadId: $threadId }) {
|
|
18
|
+
thread { isResolved }
|
|
19
|
+
}
|
|
20
|
+
}`,
|
|
21
|
+
{ threadId }
|
|
22
|
+
);
|
|
23
|
+
return { success: true };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Otherwise, try to find the review comment and resolve its thread via node_id
|
|
27
|
+
if (commentId) {
|
|
28
|
+
// Fetch the review comment to get its node_id for the thread
|
|
29
|
+
const { data: comment } = await octokit.pulls.getReviewComment({
|
|
30
|
+
owner, repo, comment_id: commentId,
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
if (comment.node_id) {
|
|
34
|
+
// Attempt to resolve the thread this comment belongs to
|
|
35
|
+
// The comment's node_id is not the thread node_id, so we need to query for the thread
|
|
36
|
+
const { repository } = await octokit.graphql(
|
|
37
|
+
`query($owner: String!, $repo: String!, $prNumber: Int!) {
|
|
38
|
+
repository(owner: $owner, name: $repo) {
|
|
39
|
+
pullRequest(number: $prNumber) {
|
|
40
|
+
reviewThreads(first: 100) {
|
|
41
|
+
nodes {
|
|
42
|
+
id
|
|
43
|
+
isResolved
|
|
44
|
+
comments(first: 1) {
|
|
45
|
+
nodes { databaseId }
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}`,
|
|
52
|
+
{ owner, repo, prNumber }
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
const threads = repository?.pullRequest?.reviewThreads?.nodes;
|
|
56
|
+
if (!threads) {
|
|
57
|
+
throw new Error(`Pull request ${prNumber} was not found or is not accessible`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const thread = threads.find(
|
|
61
|
+
t => t.comments.nodes.some(c => c.databaseId === commentId)
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
if (thread && !thread.isResolved) {
|
|
65
|
+
await octokit.graphql(
|
|
66
|
+
`mutation($threadId: ID!) {
|
|
67
|
+
resolveReviewThread(input: { threadId: $threadId }) {
|
|
68
|
+
thread { isResolved }
|
|
69
|
+
}
|
|
70
|
+
}`,
|
|
71
|
+
{ threadId: thread.id }
|
|
72
|
+
);
|
|
73
|
+
return { success: true };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (thread?.isResolved) {
|
|
77
|
+
return { success: true, message: 'Thread already resolved' };
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
throw new Error(`Could not find a review thread for comment ${commentId}`);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
throw new Error('Either commentId or threadId is required');
|
|
85
|
+
}
|
package/src/scm/gitlab/index.js
CHANGED
|
@@ -4,3 +4,4 @@ export { listPullRequestComments } from './listPullRequestComments.js';
|
|
|
4
4
|
export { listCodeReviews } from './listCodeReviews.js';
|
|
5
5
|
export { getCodeReview } from './getCodeReview.js';
|
|
6
6
|
export { searchComments } from './searchComments.js';
|
|
7
|
+
export { resolveConversation } from './resolveConversation.js';
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { getClient, projectId } from './client.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Resolve a discussion (conversation) on a GitLab merge request.
|
|
5
|
+
*/
|
|
6
|
+
export async function resolveConversation({ name, prNumber, discussionId }) {
|
|
7
|
+
if (!discussionId) throw new Error('discussionId is required to resolve a GitLab discussion');
|
|
8
|
+
|
|
9
|
+
const api = getClient();
|
|
10
|
+
|
|
11
|
+
await api.MergeRequestDiscussions.edit(projectId(name), prNumber, discussionId, { resolved: true });
|
|
12
|
+
|
|
13
|
+
return { success: true };
|
|
14
|
+
}
|
package/src/scm/index.js
CHANGED
|
@@ -94,3 +94,4 @@ export function listPullRequestComments(opts) { return getProvider(opts.remote).
|
|
|
94
94
|
export function listCodeReviews(opts) { return getProvider(opts.remote).listCodeReviews(opts); }
|
|
95
95
|
export function getCodeReview(opts) { return getProvider(opts.remote).getCodeReview(opts); }
|
|
96
96
|
export function searchComments(opts) { return getProvider(opts.remote).searchComments(opts); }
|
|
97
|
+
export function resolveConversation(opts) { return getProvider(opts.remote).resolveConversation(opts); }
|
|
@@ -5,6 +5,7 @@ import { minimatch } from 'minimatch';
|
|
|
5
5
|
|
|
6
6
|
const MAX_REVIEW_FILES = 10;
|
|
7
7
|
const MAX_FILE_LINES = 5000;
|
|
8
|
+
const MAX_FILE_SIZE_BYTES = 200 * 1024; // 200 KB
|
|
8
9
|
|
|
9
10
|
const EXCLUDED_FILENAMES = new Set([
|
|
10
11
|
'package-lock.json',
|
|
@@ -37,6 +38,10 @@ const EXCLUDED_EXTENSIONS = new Set([
|
|
|
37
38
|
'.pb', '.proto.bin',
|
|
38
39
|
// PDF / docs
|
|
39
40
|
'.pdf', '.doc', '.docx', '.xls', '.xlsx', '.ppt', '.pptx',
|
|
41
|
+
// Data / config (often large, auto-generated)
|
|
42
|
+
'.csv', '.tsv', '.parquet', '.avro', '.ndjson',
|
|
43
|
+
// Database
|
|
44
|
+
'.sqlite', '.db',
|
|
40
45
|
]);
|
|
41
46
|
|
|
42
47
|
function matchesPatterns(filePath, includePatterns, excludePatterns) {
|
|
@@ -111,10 +116,15 @@ class ReviewApiHelper extends CommonApiHelper {
|
|
|
111
116
|
continue;
|
|
112
117
|
}
|
|
113
118
|
|
|
114
|
-
// Skip files exceeding line
|
|
119
|
+
// Skip files exceeding size or line limits to avoid blowing up the AI context window
|
|
115
120
|
try {
|
|
116
121
|
const fullPath = path.resolve(gitRoot, f);
|
|
117
122
|
if (fs.existsSync(fullPath)) {
|
|
123
|
+
const stats = fs.statSync(fullPath);
|
|
124
|
+
if (stats.size > MAX_FILE_SIZE_BYTES) {
|
|
125
|
+
skipped.push({ file: f, reason: `too large (${(stats.size / 1024).toFixed(0)} KB, max ${MAX_FILE_SIZE_BYTES / 1024} KB)` });
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
118
128
|
const content = fs.readFileSync(fullPath, 'utf-8');
|
|
119
129
|
const lineCount = content.split('\n').length;
|
|
120
130
|
if (lineCount > MAX_FILE_LINES) {
|
|
@@ -214,6 +224,12 @@ class ReviewApiHelper extends CommonApiHelper {
|
|
|
214
224
|
const hunks = fileHunks.get(f);
|
|
215
225
|
const content = hunks[0]?.head_file_str || '';
|
|
216
226
|
if (content) {
|
|
227
|
+
const contentSize = Buffer.byteLength(content, 'utf-8');
|
|
228
|
+
if (contentSize > MAX_FILE_SIZE_BYTES) {
|
|
229
|
+
skippedLargeFiles.add(f);
|
|
230
|
+
dedupedSkipped.push({ file: f, reason: `too large (${(contentSize / 1024).toFixed(0)} KB, max ${MAX_FILE_SIZE_BYTES / 1024} KB)` });
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
217
233
|
const lineCount = content.split('\n').length;
|
|
218
234
|
if (lineCount > MAX_FILE_LINES) {
|
|
219
235
|
skippedLargeFiles.add(f);
|
|
@@ -48,9 +48,14 @@ const EXCLUDED_EXTENSIONS = new Set([
|
|
|
48
48
|
'.pb', '.proto.bin',
|
|
49
49
|
// PDF / docs
|
|
50
50
|
'.pdf', '.doc', '.docx', '.xls', '.xlsx', '.ppt', '.pptx',
|
|
51
|
+
// Data / config (often large, auto-generated)
|
|
52
|
+
'.parquet', '.avro',
|
|
53
|
+
// Database
|
|
54
|
+
'.sqlite', '.db',
|
|
51
55
|
]);
|
|
52
56
|
|
|
53
57
|
const MAX_FILE_LINES = 5000;
|
|
58
|
+
const MAX_FILE_SIZE_BYTES = 200 * 1024; // 200 KB
|
|
54
59
|
|
|
55
60
|
function isScannableFile(filePath) {
|
|
56
61
|
const basename = path.basename(filePath);
|
|
@@ -201,8 +206,13 @@ class SecretsApiHelper extends CommonApiHelper {
|
|
|
201
206
|
return false;
|
|
202
207
|
}
|
|
203
208
|
|
|
204
|
-
// Skip files exceeding line
|
|
209
|
+
// Skip files exceeding size or line limits
|
|
205
210
|
if (fileObj.code) {
|
|
211
|
+
const contentSize = Buffer.byteLength(fileObj.code, 'utf-8');
|
|
212
|
+
if (contentSize > MAX_FILE_SIZE_BYTES) {
|
|
213
|
+
skipped.push({ file: filePath, reason: `too large (${(contentSize / 1024).toFixed(0)} KB, max ${MAX_FILE_SIZE_BYTES / 1024} KB)` });
|
|
214
|
+
return false;
|
|
215
|
+
}
|
|
206
216
|
const lineCount = fileObj.code.split('\n').length;
|
|
207
217
|
if (lineCount > MAX_FILE_LINES) {
|
|
208
218
|
skipped.push({ file: filePath, reason: `too large (${lineCount} lines, max ${MAX_FILE_LINES})` });
|