codeant-cli 0.3.3 → 0.3.5
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 +8 -10
- package/src/commands/getBaseUrl.js +21 -0
- package/src/commands/login.js +120 -0
- package/src/commands/logout.js +30 -0
- package/src/commands/review.js +294 -0
- package/src/commands/secrets.js +107 -0
- package/src/commands/setBaseUrl.js +37 -0
- package/src/components/ReviewUI.js +410 -0
- package/src/components/SecretsUI.js +366 -0
- package/src/components/Welcome.js +144 -0
- package/src/index.js +390 -0
- package/src/reviewHeadless.js +198 -0
- package/src/rules/secrets.toml +4994 -0
- package/src/scm/azure/client.js +21 -0
- package/src/scm/azure/getCodeReview.js +20 -0
- package/src/scm/azure/getPullRequest.js +30 -0
- package/src/scm/azure/index.js +7 -0
- package/src/scm/azure/listCodeReviews.js +17 -0
- package/src/scm/azure/listPullRequestComments.js +41 -0
- package/src/scm/azure/listPullRequests.js +26 -0
- package/src/scm/azure/resolveConversation.js +22 -0
- package/src/scm/azure/searchComments.js +35 -0
- package/src/scm/bitbucket/client.js +18 -0
- package/src/scm/bitbucket/getCodeReview.js +3 -0
- package/src/scm/bitbucket/getPullRequest.js +26 -0
- package/src/scm/bitbucket/index.js +7 -0
- package/src/scm/bitbucket/listCodeReviews.js +17 -0
- package/src/scm/bitbucket/listPullRequestComments.js +33 -0
- package/src/scm/bitbucket/listPullRequests.js +33 -0
- package/src/scm/bitbucket/resolveConversation.js +21 -0
- package/src/scm/bitbucket/searchComments.js +29 -0
- package/src/scm/github/client.js +17 -0
- package/src/scm/github/getCodeReview.js +26 -0
- package/src/scm/github/getPullRequest.js +27 -0
- package/src/scm/github/index.js +7 -0
- package/src/scm/github/listCodeReviews.js +17 -0
- package/src/scm/github/listPullRequestComments.js +51 -0
- package/src/scm/github/listPullRequests.js +32 -0
- package/src/scm/github/resolveConversation.js +85 -0
- package/src/scm/github/searchComments.js +30 -0
- package/src/scm/gitlab/client.js +16 -0
- package/src/scm/gitlab/getCodeReview.js +18 -0
- package/src/scm/gitlab/getPullRequest.js +24 -0
- package/src/scm/gitlab/index.js +7 -0
- package/src/scm/gitlab/listCodeReviews.js +21 -0
- package/src/scm/gitlab/listPullRequestComments.js +44 -0
- package/src/scm/gitlab/listPullRequests.js +26 -0
- package/src/scm/gitlab/resolveConversation.js +14 -0
- package/src/scm/gitlab/searchComments.js +28 -0
- package/src/scm/index.js +97 -0
- package/src/tools/executeTool.js +31 -0
- package/src/tools/globTool.js +9 -0
- package/src/tools/grepTool.js +18 -0
- package/src/tools/lsTool.js +7 -0
- package/src/tools/readTool.js +12 -0
- package/src/utils/analytics.js +56 -0
- package/src/utils/baseUrl.js +11 -0
- package/src/utils/commonApiHelper.js +143 -0
- package/src/utils/config.js +42 -0
- package/src/utils/fetchApi.js +39 -0
- package/src/utils/gitDiffHelper.js +1203 -0
- package/src/utils/reviewApiHelper.js +291 -0
- package/src/utils/scmAuth.js +136 -0
- package/src/utils/secretsApiHelper.js +252 -0
- package/src/utils/secretsDetector.js +210 -0
- package/dist/178.index.js +0 -1
- package/dist/239.index.js +0 -1
- package/dist/701.index.js +0 -1
- package/dist/880.index.js +0 -1
- package/dist/935.index.js +0 -1
- package/dist/966.index.js +0 -1
- package/dist/index.js +0 -28
- package/dist/package.json +0 -1
- package/dist/review/239.index.js +0 -1
- package/dist/review/index.js +0 -8
- package/dist/review/package.json +0 -3
package/src/index.js
ADDED
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { program } from 'commander';
|
|
4
|
+
import { render } from 'ink';
|
|
5
|
+
import React from 'react';
|
|
6
|
+
import { createRequire } from 'module';
|
|
7
|
+
import Secrets from './commands/secrets.js';
|
|
8
|
+
import SetBaseUrl from './commands/setBaseUrl.js';
|
|
9
|
+
import GetBaseUrl from './commands/getBaseUrl.js';
|
|
10
|
+
import Login from './commands/login.js';
|
|
11
|
+
import Logout from './commands/logout.js';
|
|
12
|
+
import Review from './commands/review.js';
|
|
13
|
+
import Welcome from './components/Welcome.js';
|
|
14
|
+
import * as scm from './scm/index.js';
|
|
15
|
+
import { setConfigValue } from './utils/config.js';
|
|
16
|
+
import { track, shutdown as analyticsShutdown, isTelemetryDisabled } from './utils/analytics.js';
|
|
17
|
+
|
|
18
|
+
// Read version from package.json
|
|
19
|
+
const require = createRequire(import.meta.url);
|
|
20
|
+
const pkg = require('../package.json');
|
|
21
|
+
|
|
22
|
+
// Split comma-separated globs while preserving commas inside {} brace expansions (e.g. "*.{js,ts}")
|
|
23
|
+
function splitGlobs(input) {
|
|
24
|
+
const parts = [];
|
|
25
|
+
let current = '';
|
|
26
|
+
let depth = 0;
|
|
27
|
+
for (const ch of String(input)) {
|
|
28
|
+
if (ch === '{') depth++;
|
|
29
|
+
else if (ch === '}') depth--;
|
|
30
|
+
if (ch === ',' && depth === 0) {
|
|
31
|
+
parts.push(current.trim());
|
|
32
|
+
current = '';
|
|
33
|
+
} else {
|
|
34
|
+
current += ch;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
if (current.trim()) parts.push(current.trim());
|
|
38
|
+
return parts.filter(Boolean);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Show welcome animation if no arguments provided
|
|
42
|
+
if (process.argv.length === 2) {
|
|
43
|
+
render(React.createElement(Welcome, { version: pkg.version }));
|
|
44
|
+
} else {
|
|
45
|
+
program
|
|
46
|
+
.name('codeant')
|
|
47
|
+
.description('Code review CLI tool')
|
|
48
|
+
.version(pkg.version);
|
|
49
|
+
|
|
50
|
+
program
|
|
51
|
+
.command('secrets')
|
|
52
|
+
.description('Scan for secrets in your code')
|
|
53
|
+
.option('--all', 'Scan committed + uncommitted changes (default)')
|
|
54
|
+
.option('--committed', 'Scan only unpushed commits')
|
|
55
|
+
.option('--uncommitted', 'Scan only uncommitted changes (staged + unstaged + untracked)')
|
|
56
|
+
.option('--staged', 'Scan only staged files')
|
|
57
|
+
.option('--last-commit', 'Scan last commit')
|
|
58
|
+
.option('--last-n-commits <n>', 'Scan last n commits (max 5)', parseInt)
|
|
59
|
+
.option('--base <branch>', 'Compare against a specific base branch (e.g. --base develop)')
|
|
60
|
+
.option('--base-commit <commit>', 'Compare against a specific commit (e.g. --base-commit HEAD~3)')
|
|
61
|
+
.option('--fail-on <level>', 'Fail on issues at or above this level: BLOCKER, CRITICAL, MAJOR, MINOR, INFO (default: CRITICAL)', 'CRITICAL')
|
|
62
|
+
.option('--include <paths>', 'Comma-separated list of file paths glob patterns to include')
|
|
63
|
+
.option('--exclude <paths>', 'Comma-separated list of file paths glob patterns to exclude')
|
|
64
|
+
.action((options) => {
|
|
65
|
+
let scanType = 'all';
|
|
66
|
+
let lastNCommits = 1;
|
|
67
|
+
let baseBranch = null;
|
|
68
|
+
let baseCommit = null;
|
|
69
|
+
|
|
70
|
+
if (options.base) {
|
|
71
|
+
scanType = 'base-branch';
|
|
72
|
+
baseBranch = options.base;
|
|
73
|
+
} else if (options.baseCommit) {
|
|
74
|
+
scanType = 'base-commit';
|
|
75
|
+
baseCommit = options.baseCommit;
|
|
76
|
+
// Check !== undefined, not truthy — passing 0 is valid and shouldn't fall through
|
|
77
|
+
} else if (options.lastNCommits !== undefined) {
|
|
78
|
+
scanType = 'last-n-commits';
|
|
79
|
+
lastNCommits = Math.min(Math.max(1, options.lastNCommits), 5);
|
|
80
|
+
} else if (options.committed) {
|
|
81
|
+
scanType = 'committed';
|
|
82
|
+
} else if (options.uncommitted) {
|
|
83
|
+
scanType = 'uncommitted';
|
|
84
|
+
} else if (options.staged) {
|
|
85
|
+
scanType = 'staged-only';
|
|
86
|
+
} else if (options.lastCommit) {
|
|
87
|
+
scanType = 'last-commit';
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const include = options.include
|
|
91
|
+
? (Array.isArray(options.include) ? options.include : splitGlobs(options.include))
|
|
92
|
+
: [];
|
|
93
|
+
|
|
94
|
+
const exclude = options.exclude
|
|
95
|
+
? (Array.isArray(options.exclude) ? options.exclude : splitGlobs(options.exclude))
|
|
96
|
+
: [];
|
|
97
|
+
|
|
98
|
+
const failOn = options.failOn?.toUpperCase() || 'CRITICAL';
|
|
99
|
+
|
|
100
|
+
render(React.createElement(Secrets, { scanType, failOn, include, exclude, lastNCommits, baseBranch, baseCommit }));
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
program
|
|
104
|
+
.command('review')
|
|
105
|
+
.description('Run AI-powered code review')
|
|
106
|
+
.option('--all', 'Review committed + uncommitted changes (default)')
|
|
107
|
+
.option('--committed', 'Review only unpushed commits')
|
|
108
|
+
.option('--uncommitted', 'Review only uncommitted changes (staged + unstaged + untracked)')
|
|
109
|
+
.option('--staged', 'Review only staged files')
|
|
110
|
+
.option('--last-commit', 'Review last commit')
|
|
111
|
+
.option('--last-n-commits <n>', 'Review last n commits (max 5)', parseInt)
|
|
112
|
+
.option('--base <branch>', 'Compare against a specific base branch (e.g. --base develop)')
|
|
113
|
+
.option('--base-commit <commit>', 'Compare against a specific commit (e.g. --base-commit HEAD~3)')
|
|
114
|
+
.option('--fail-on <level>', 'Fail on issues at or above this level: BLOCKER, CRITICAL, MAJOR, MINOR, INFO (default: CRITICAL)', 'CRITICAL')
|
|
115
|
+
.option('--include <paths>', 'Comma-separated list of file paths glob patterns to include')
|
|
116
|
+
.option('--exclude <paths>', 'Comma-separated list of file paths glob patterns to exclude')
|
|
117
|
+
.action((options) => {
|
|
118
|
+
let scanType = 'all';
|
|
119
|
+
let lastNCommits = 1;
|
|
120
|
+
let baseBranch = null;
|
|
121
|
+
let baseCommit = null;
|
|
122
|
+
|
|
123
|
+
if (options.base) {
|
|
124
|
+
scanType = 'base-branch';
|
|
125
|
+
baseBranch = options.base;
|
|
126
|
+
} else if (options.baseCommit) {
|
|
127
|
+
scanType = 'base-commit';
|
|
128
|
+
baseCommit = options.baseCommit;
|
|
129
|
+
// Check !== undefined, not truthy — passing 0 is valid and shouldn't fall through
|
|
130
|
+
} else if (options.lastNCommits !== undefined) {
|
|
131
|
+
scanType = 'last-n-commits';
|
|
132
|
+
lastNCommits = Math.min(Math.max(1, options.lastNCommits), 5);
|
|
133
|
+
} else if (options.committed) {
|
|
134
|
+
scanType = 'committed';
|
|
135
|
+
} else if (options.uncommitted) {
|
|
136
|
+
scanType = 'uncommitted';
|
|
137
|
+
} else if (options.staged) {
|
|
138
|
+
scanType = 'staged-only';
|
|
139
|
+
} else if (options.lastCommit) {
|
|
140
|
+
scanType = 'last-commit';
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const include = options.include
|
|
144
|
+
? (Array.isArray(options.include) ? options.include : splitGlobs(options.include))
|
|
145
|
+
: [];
|
|
146
|
+
|
|
147
|
+
const exclude = options.exclude
|
|
148
|
+
? (Array.isArray(options.exclude) ? options.exclude : splitGlobs(options.exclude))
|
|
149
|
+
: [];
|
|
150
|
+
|
|
151
|
+
const failOn = options.failOn?.toUpperCase() || 'CRITICAL';
|
|
152
|
+
|
|
153
|
+
render(React.createElement(Review, { scanType, lastNCommits, failOn, include, exclude, baseBranch, baseCommit }));
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
program
|
|
157
|
+
.command('set-base-url <url>')
|
|
158
|
+
.description('Set the API base URL')
|
|
159
|
+
.action((url) => {
|
|
160
|
+
render(React.createElement(SetBaseUrl, { url }));
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
program
|
|
164
|
+
.command('get-base-url')
|
|
165
|
+
.description('Show the current API base URL')
|
|
166
|
+
.action(() => {
|
|
167
|
+
render(React.createElement(GetBaseUrl));
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
program
|
|
171
|
+
.command('login')
|
|
172
|
+
.description('Login to CodeAnt')
|
|
173
|
+
.action(() => {
|
|
174
|
+
render(React.createElement(Login));
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
program
|
|
178
|
+
.command('logout')
|
|
179
|
+
.description('Logout from CodeAnt')
|
|
180
|
+
.action(() => {
|
|
181
|
+
render(React.createElement(Logout));
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
// ─── Helper: resolve repo params with auto-detection fallback ───
|
|
185
|
+
function resolveRepoOpts(options) {
|
|
186
|
+
const remote = options.remote || scm.detectRemote();
|
|
187
|
+
const name = options.name || scm.detectRepoName();
|
|
188
|
+
const defaultBranch = options.defaultBranch || scm.detectDefaultBranch();
|
|
189
|
+
if (!remote) { console.error('Error: Could not detect remote. Use --remote (github|gitlab|bitbucket|azure)'); process.exit(1); }
|
|
190
|
+
if (!name) { console.error('Error: Could not detect repo name. Use --name owner/repo'); process.exit(1); }
|
|
191
|
+
return { ...options, remote, name, defaultBranch };
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Helper: run async command and output JSON
|
|
195
|
+
async function runCmd(fn) {
|
|
196
|
+
try {
|
|
197
|
+
const result = await fn();
|
|
198
|
+
console.log(JSON.stringify(result, null, 2));
|
|
199
|
+
} catch (err) {
|
|
200
|
+
console.error(JSON.stringify({ error: err.message }, null, 2));
|
|
201
|
+
process.exit(1);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// ─── Token management ───
|
|
206
|
+
program
|
|
207
|
+
.command('set-token <remote> <token>')
|
|
208
|
+
.description('Store auth token for a platform (github|gitlab|bitbucket|azure)')
|
|
209
|
+
.action((remote, token) => {
|
|
210
|
+
const keyMap = { github: 'githubToken', gitlab: 'gitlabToken', bitbucket: 'bitbucketToken', azure: 'azureDevOpsToken' };
|
|
211
|
+
const key = keyMap[remote];
|
|
212
|
+
if (!key) { console.error(`Unknown remote "${remote}". Use: github, gitlab, bitbucket, azure`); process.exit(1); }
|
|
213
|
+
setConfigValue(key, token);
|
|
214
|
+
console.log(`${remote} token saved.`);
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
// ─── PR commands ───
|
|
218
|
+
const pr = program.command('pr').description('Pull request tools (GitHub, GitLab, Bitbucket, Azure DevOps)');
|
|
219
|
+
|
|
220
|
+
pr.command('list')
|
|
221
|
+
.description('List pull requests / merge requests')
|
|
222
|
+
.option('--name <repo>', 'Repository (owner/repo)')
|
|
223
|
+
.option('--remote <provider>', 'github, gitlab, bitbucket, azure')
|
|
224
|
+
.option('--default-branch <branch>', 'Default branch name')
|
|
225
|
+
.option('--source-branch <branch>', 'Filter by source branch (partial match)')
|
|
226
|
+
.option('--author <login>', 'Filter by author (fuzzy match)')
|
|
227
|
+
.option('--state <state>', 'open or closed (default: open)', 'open')
|
|
228
|
+
.option('--limit <n>', 'Max results (default: 20, max: 100)', parseInt, 20)
|
|
229
|
+
.option('--offset <n>', 'Pagination offset', parseInt, 0)
|
|
230
|
+
.action((options) => {
|
|
231
|
+
const opts = resolveRepoOpts(options);
|
|
232
|
+
runCmd(() => scm.listPullRequests({
|
|
233
|
+
name: opts.name, remote: opts.remote, defaultBranch: opts.defaultBranch,
|
|
234
|
+
sourceBranch: opts.sourceBranch, authorLogin: opts.author,
|
|
235
|
+
state: opts.state, limit: opts.limit, offset: opts.offset,
|
|
236
|
+
}));
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
pr.command('get')
|
|
240
|
+
.description('Get detailed PR information including review analysis')
|
|
241
|
+
.option('--name <repo>', 'Repository (owner/repo)')
|
|
242
|
+
.option('--remote <provider>', 'github, gitlab, bitbucket, azure')
|
|
243
|
+
.option('--default-branch <branch>', 'Default branch name')
|
|
244
|
+
.requiredOption('--pr-number <n>', 'PR number', parseInt)
|
|
245
|
+
.action((options) => {
|
|
246
|
+
const opts = resolveRepoOpts(options);
|
|
247
|
+
runCmd(() => scm.getPullRequest({
|
|
248
|
+
name: opts.name, remote: opts.remote, defaultBranch: opts.defaultBranch,
|
|
249
|
+
prNumber: opts.prNumber,
|
|
250
|
+
}));
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
pr.command('comments')
|
|
254
|
+
.description('List all comments for a PR')
|
|
255
|
+
.option('--name <repo>', 'Repository (owner/repo)')
|
|
256
|
+
.option('--remote <provider>', 'github, gitlab, bitbucket, azure')
|
|
257
|
+
.option('--default-branch <branch>', 'Default branch name')
|
|
258
|
+
.requiredOption('--pr-number <n>', 'PR number', parseInt)
|
|
259
|
+
.option('--codeant-generated <bool>', 'Filter by CodeAnt authorship (true/false)', (v) => v === 'true')
|
|
260
|
+
.option('--addressed', 'Filter by addressed/resolved status')
|
|
261
|
+
.option('--created-after <date>', 'ISO 8601 date filter')
|
|
262
|
+
.option('--created-before <date>', 'ISO 8601 date filter')
|
|
263
|
+
.action((options) => {
|
|
264
|
+
const opts = resolveRepoOpts(options);
|
|
265
|
+
runCmd(() => scm.listPullRequestComments({
|
|
266
|
+
name: opts.name, remote: opts.remote, defaultBranch: opts.defaultBranch,
|
|
267
|
+
prNumber: opts.prNumber, codeantGenerated: opts.codeantGenerated,
|
|
268
|
+
addressed: opts.addressed, createdAfter: opts.createdAfter, createdBefore: opts.createdBefore,
|
|
269
|
+
}));
|
|
270
|
+
});
|
|
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
|
+
|
|
291
|
+
// ─── Code review commands ───
|
|
292
|
+
const codeReview = program.command('code-review').description('Code review tools');
|
|
293
|
+
|
|
294
|
+
codeReview.command('list')
|
|
295
|
+
.description('List code reviews with optional filtering')
|
|
296
|
+
.option('--name <repo>', 'Repository (owner/repo)')
|
|
297
|
+
.option('--remote <provider>', 'github, gitlab, bitbucket, azure')
|
|
298
|
+
.option('--default-branch <branch>', 'Default branch name')
|
|
299
|
+
.option('--pr-number <n>', 'Filter by PR number', parseInt)
|
|
300
|
+
.option('--status <status>', 'Filter by status (PENDING, COMPLETED, FAILED, etc.)')
|
|
301
|
+
.option('--limit <n>', 'Max results (default: 20)', parseInt, 20)
|
|
302
|
+
.option('--offset <n>', 'Pagination offset', parseInt, 0)
|
|
303
|
+
.action((options) => {
|
|
304
|
+
const opts = resolveRepoOpts(options);
|
|
305
|
+
runCmd(() => scm.listCodeReviews({
|
|
306
|
+
name: opts.name, remote: opts.remote, defaultBranch: opts.defaultBranch,
|
|
307
|
+
prNumber: opts.prNumber, status: opts.status,
|
|
308
|
+
limit: opts.limit, offset: opts.offset,
|
|
309
|
+
}));
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
codeReview.command('get')
|
|
313
|
+
.description('Get detailed information for a specific code review')
|
|
314
|
+
.option('--name <repo>', 'Repository (owner/repo)')
|
|
315
|
+
.option('--remote <provider>', 'github, gitlab, bitbucket, azure')
|
|
316
|
+
.requiredOption('--pr-number <n>', 'PR number', parseInt)
|
|
317
|
+
.requiredOption('--review-id <id>', 'Code review ID')
|
|
318
|
+
.action((options) => {
|
|
319
|
+
const opts = resolveRepoOpts(options);
|
|
320
|
+
runCmd(() => scm.getCodeReview({
|
|
321
|
+
name: opts.name, remote: opts.remote,
|
|
322
|
+
prNumber: opts.prNumber, reviewId: opts.reviewId,
|
|
323
|
+
}));
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
// ─── Comment search ───
|
|
327
|
+
const comments = program.command('comments').description('Comment search tools');
|
|
328
|
+
|
|
329
|
+
comments.command('search')
|
|
330
|
+
.description('Search across all CodeAnt comments')
|
|
331
|
+
.option('--name <repo>', 'Repository (owner/repo)')
|
|
332
|
+
.option('--remote <provider>', 'github, gitlab, bitbucket, azure')
|
|
333
|
+
.requiredOption('--query <term>', 'Search term')
|
|
334
|
+
.option('--limit <n>', 'Max results (default: 10, max: 50)', parseInt, 10)
|
|
335
|
+
.option('--include-addressed', 'Include resolved comments (default: false)')
|
|
336
|
+
.option('--created-after <date>', 'ISO 8601 date filter')
|
|
337
|
+
.action((options) => {
|
|
338
|
+
const opts = resolveRepoOpts(options);
|
|
339
|
+
runCmd(() => scm.searchComments({
|
|
340
|
+
name: opts.name, remote: opts.remote,
|
|
341
|
+
query: opts.query, limit: opts.limit,
|
|
342
|
+
includeAddressed: opts.includeAddressed || false,
|
|
343
|
+
createdAfter: opts.createdAfter,
|
|
344
|
+
}));
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
// ─── Telemetry control ───
|
|
348
|
+
program
|
|
349
|
+
.command('set-telemetry <enabled>')
|
|
350
|
+
.description('Enable or disable telemetry / PostHog analytics (true or false)')
|
|
351
|
+
.action((enabled) => {
|
|
352
|
+
const val = enabled.toLowerCase();
|
|
353
|
+
if (val !== 'true' && val !== 'false') {
|
|
354
|
+
console.error('Usage: codeant set-telemetry <true|false>');
|
|
355
|
+
process.exit(1);
|
|
356
|
+
}
|
|
357
|
+
setConfigValue('telemetryEnabled', val === 'true');
|
|
358
|
+
console.log(`Telemetry ${val === 'true' ? 'enabled' : 'disabled'}.`);
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
program
|
|
362
|
+
.command('get-telemetry')
|
|
363
|
+
.description('Show current telemetry status')
|
|
364
|
+
.action(() => {
|
|
365
|
+
const disabled = isTelemetryDisabled();
|
|
366
|
+
console.log(`Telemetry is currently ${disabled ? 'disabled' : 'enabled'}.`);
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
// ─── Analytics tracking (for external callers like Claude Code skills) ───
|
|
370
|
+
program
|
|
371
|
+
.command('track')
|
|
372
|
+
.description('Send an analytics event')
|
|
373
|
+
.requiredOption('--event <name>', 'Event name (e.g. skill_invoked, suggestions_applied)')
|
|
374
|
+
.option('--props <json>', 'JSON string of event properties', '{}')
|
|
375
|
+
.action(async (options) => {
|
|
376
|
+
try {
|
|
377
|
+
const props = JSON.parse(options.props);
|
|
378
|
+
if (typeof props !== 'object' || props === null || Array.isArray(props)) {
|
|
379
|
+
process.stderr.write('--props must be a JSON object\n');
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
track(options.event, props);
|
|
383
|
+
await analyticsShutdown();
|
|
384
|
+
} catch (err) {
|
|
385
|
+
process.stderr.write(`Invalid --props JSON: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
386
|
+
}
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
program.parse();
|
|
390
|
+
}
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import ReviewApiHelper from './utils/reviewApiHelper.js';
|
|
2
|
+
import { fetchApi } from './utils/fetchApi.js';
|
|
3
|
+
import { executeTool } from './tools/executeTool.js';
|
|
4
|
+
import { getConfigValue, setConfigValue } from './utils/config.js';
|
|
5
|
+
import { track } from './utils/analytics.js';
|
|
6
|
+
|
|
7
|
+
const MAX_TURNS = 5;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Run a single agent turn loop (per-file or reflector).
|
|
11
|
+
*/
|
|
12
|
+
async function runTurnLoop(initialPayload, gitRoot, isReflectorLoop) {
|
|
13
|
+
let nextPayload = initialPayload;
|
|
14
|
+
let finalMessage = null;
|
|
15
|
+
let finalOutput = null;
|
|
16
|
+
|
|
17
|
+
for (let turn = 0; turn < MAX_TURNS; turn += 1) {
|
|
18
|
+
const resp = await fetchApi('/extension/pr-review/agent/turn', 'POST', nextPayload);
|
|
19
|
+
|
|
20
|
+
if (!resp || typeof resp !== 'object') {
|
|
21
|
+
throw new Error('Invalid response from agent API');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const sessionId = resp.session_id;
|
|
25
|
+
const assistantMsg = resp.response;
|
|
26
|
+
const done = resp.done;
|
|
27
|
+
|
|
28
|
+
if (!sessionId) {
|
|
29
|
+
throw new Error('Missing session_id in agent response');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
finalMessage = assistantMsg;
|
|
33
|
+
if (resp.output) finalOutput = resp.output;
|
|
34
|
+
|
|
35
|
+
if (done) {
|
|
36
|
+
if (isReflectorLoop && resp.parsing_error) {
|
|
37
|
+
console.error('Warning: parsing error in reflector response');
|
|
38
|
+
}
|
|
39
|
+
break;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const toolCalls = assistantMsg?.tool_calls || [];
|
|
43
|
+
if (toolCalls.length === 0) break;
|
|
44
|
+
|
|
45
|
+
const toolResults = await Promise.all(
|
|
46
|
+
toolCalls.map(async (tc) => ({
|
|
47
|
+
tool_call_id: tc.id,
|
|
48
|
+
content: await executeTool(tc, gitRoot),
|
|
49
|
+
}))
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
nextPayload = { session_id: sessionId, tool_results: toolResults };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return { finalMessage, finalOutput };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Headless review runner — no React/Ink, returns plain JSON.
|
|
60
|
+
*
|
|
61
|
+
* @param {Object} options
|
|
62
|
+
* @param {string} options.workspacePath - Absolute path to the repo/workspace
|
|
63
|
+
* @param {string} [options.apiKey] - API key (overrides env/config)
|
|
64
|
+
* @param {string} [options.baseUrl] - API base URL (overrides env/config)
|
|
65
|
+
* @param {string} [options.scanType='all'] - all|committed|uncommitted|staged-only|last-commit|last-n-commits|base-branch|base-commit
|
|
66
|
+
* @param {string[]} [options.include=[]] - Glob patterns to include
|
|
67
|
+
* @param {string[]} [options.exclude=[]] - Glob patterns to exclude
|
|
68
|
+
* @param {number} [options.lastNCommits=1]
|
|
69
|
+
* @param {string} [options.baseBranch]
|
|
70
|
+
* @param {string} [options.baseCommit]
|
|
71
|
+
* @param {function} [options.onProgress] - Optional callback(message) for progress updates
|
|
72
|
+
* @returns {Promise<{issues: Array, meta: Object|null, error: string|null}>}
|
|
73
|
+
*/
|
|
74
|
+
export async function runReviewHeadless(options = {}) {
|
|
75
|
+
const {
|
|
76
|
+
workspacePath,
|
|
77
|
+
apiKey,
|
|
78
|
+
baseUrl,
|
|
79
|
+
scanType = 'all',
|
|
80
|
+
include = [],
|
|
81
|
+
exclude = [],
|
|
82
|
+
lastNCommits = 1,
|
|
83
|
+
baseBranch = null,
|
|
84
|
+
baseCommit = null,
|
|
85
|
+
onProgress = () => {},
|
|
86
|
+
onFilesReady = () => {},
|
|
87
|
+
} = options;
|
|
88
|
+
|
|
89
|
+
// If the CLI config doesn't have an apiKey but the extension passed one, persist it
|
|
90
|
+
if (apiKey && !getConfigValue('apiKey')) {
|
|
91
|
+
setConfigValue('apiKey', apiKey);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Temporarily set env vars so fetchApi picks them up
|
|
95
|
+
const prevToken = process.env.CODEANT_API_TOKEN;
|
|
96
|
+
const prevUrl = process.env.CODEANT_API_URL;
|
|
97
|
+
|
|
98
|
+
if (apiKey) process.env.CODEANT_API_TOKEN = apiKey;
|
|
99
|
+
if (baseUrl) process.env.CODEANT_API_URL = baseUrl;
|
|
100
|
+
|
|
101
|
+
const reviewStartTime = Date.now();
|
|
102
|
+
track('review_triggered', { scan_type: scanType, source: 'headless' });
|
|
103
|
+
|
|
104
|
+
try {
|
|
105
|
+
// ── Fetch diff ──────────────────────────────────────────────────────
|
|
106
|
+
onProgress('Fetching diff...');
|
|
107
|
+
|
|
108
|
+
const helper = new ReviewApiHelper(workspacePath);
|
|
109
|
+
await helper.init();
|
|
110
|
+
const gitRoot = helper.getGitRoot() || workspacePath;
|
|
111
|
+
const requestBody = await helper.buildReviewApiRequest(scanType, include, exclude, { lastNCommits, baseBranch, baseCommit });
|
|
112
|
+
|
|
113
|
+
const meta = requestBody?._meta || null;
|
|
114
|
+
delete requestBody._meta;
|
|
115
|
+
|
|
116
|
+
// Notify caller about files being reviewed as soon as we know
|
|
117
|
+
onFilesReady(meta?.reviewed_files || [], meta);
|
|
118
|
+
|
|
119
|
+
if (!requestBody?.diff_content?.length) {
|
|
120
|
+
track('review_completed', { scan_type: scanType, source: 'headless', issue_count: 0, file_count: 0, duration_ms: Date.now() - reviewStartTime, no_files: true });
|
|
121
|
+
return { issues: [], meta, error: null, noFiles: true };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// ── Split into per-file requests ────────────────────────────────────
|
|
125
|
+
const perFileRequests = ReviewApiHelper.splitIntoPerFileRequests(requestBody);
|
|
126
|
+
|
|
127
|
+
onProgress(`Analyzing ${perFileRequests.length} file${perFileRequests.length !== 1 ? 's' : ''} in parallel...`);
|
|
128
|
+
|
|
129
|
+
// ── Per-file agent turn loops (parallel) ────────────────────────────
|
|
130
|
+
const perFileResults = await Promise.all(
|
|
131
|
+
perFileRequests.map((fileReq) => {
|
|
132
|
+
const filename = fileReq._filename;
|
|
133
|
+
delete fileReq._filename;
|
|
134
|
+
|
|
135
|
+
if (fileReq.file_contents?.[filename]) {
|
|
136
|
+
fileReq.file_content = fileReq.file_contents[filename];
|
|
137
|
+
fileReq.file_path = filename;
|
|
138
|
+
}
|
|
139
|
+
delete fileReq.file_contents;
|
|
140
|
+
|
|
141
|
+
return runTurnLoop(fileReq, gitRoot, false);
|
|
142
|
+
})
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
// Pair each file's suggestions with its own diff — skip files with no real suggestions
|
|
146
|
+
const perFileWithSuggestions = perFileRequests.map((fileReq, i) => ({
|
|
147
|
+
diff_content: fileReq.diff_content,
|
|
148
|
+
suggestions: perFileResults[i].finalMessage?.content,
|
|
149
|
+
output: perFileResults[i].finalOutput,
|
|
150
|
+
})).filter(r => r.output?.code_suggestions?.length > 0);
|
|
151
|
+
|
|
152
|
+
// ── Per-file reflector loops (parallel) ──────────────────────────────
|
|
153
|
+
onProgress('Running reflector...');
|
|
154
|
+
const reflectorResults = await Promise.all(
|
|
155
|
+
perFileWithSuggestions.map(({ diff_content, suggestions }) =>
|
|
156
|
+
runTurnLoop(
|
|
157
|
+
{
|
|
158
|
+
diff_content,
|
|
159
|
+
prompt_template_name: 'reflector',
|
|
160
|
+
extra_variables: { suggestion_str: suggestions },
|
|
161
|
+
},
|
|
162
|
+
gitRoot,
|
|
163
|
+
true
|
|
164
|
+
)
|
|
165
|
+
)
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
// ── Parse results ───────────────────────────────────────────────────
|
|
169
|
+
const issues = reflectorResults.flatMap(r =>
|
|
170
|
+
(r.finalOutput?.code_suggestions || []).map((issue) => ({
|
|
171
|
+
issue_content: issue.issue_content || '',
|
|
172
|
+
relevant_file: issue.relevant_file || 'Unknown',
|
|
173
|
+
start_line: issue.start_line || 0,
|
|
174
|
+
label: issue.label || 'Code Quality',
|
|
175
|
+
}))
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
const labelCounts = {};
|
|
179
|
+
for (const i of issues) { labelCounts[i.label] = (labelCounts[i.label] || 0) + 1; }
|
|
180
|
+
track('review_completed', {
|
|
181
|
+
scan_type: scanType, source: 'headless', issue_count: issues.length,
|
|
182
|
+
file_count: perFileRequests.length, duration_ms: Date.now() - reviewStartTime,
|
|
183
|
+
label_counts: labelCounts,
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
return { issues, meta, error: null, noFiles: false };
|
|
187
|
+
} catch (err) {
|
|
188
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
189
|
+
track('review_error', { scan_type: scanType, source: 'headless', error: errMsg });
|
|
190
|
+
return { issues: [], meta: null, error: errMsg, noFiles: false };
|
|
191
|
+
} finally {
|
|
192
|
+
// Restore env vars
|
|
193
|
+
if (prevToken !== undefined) process.env.CODEANT_API_TOKEN = prevToken;
|
|
194
|
+
else delete process.env.CODEANT_API_TOKEN;
|
|
195
|
+
if (prevUrl !== undefined) process.env.CODEANT_API_URL = prevUrl;
|
|
196
|
+
else delete process.env.CODEANT_API_URL;
|
|
197
|
+
}
|
|
198
|
+
}
|