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
package/src/index.js DELETED
@@ -1,349 +0,0 @@
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 } from './utils/analytics.js';
17
-
18
- // Read version from package.json — createRequire works with ncc bundling
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
- // ─── Code review commands ───
273
- const codeReview = program.command('code-review').description('Code review tools');
274
-
275
- codeReview.command('list')
276
- .description('List code reviews with optional filtering')
277
- .option('--name <repo>', 'Repository (owner/repo)')
278
- .option('--remote <provider>', 'github, gitlab, bitbucket, azure')
279
- .option('--default-branch <branch>', 'Default branch name')
280
- .option('--pr-number <n>', 'Filter by PR number', parseInt)
281
- .option('--status <status>', 'Filter by status (PENDING, COMPLETED, FAILED, etc.)')
282
- .option('--limit <n>', 'Max results (default: 20)', parseInt, 20)
283
- .option('--offset <n>', 'Pagination offset', parseInt, 0)
284
- .action((options) => {
285
- const opts = resolveRepoOpts(options);
286
- runCmd(() => scm.listCodeReviews({
287
- name: opts.name, remote: opts.remote, defaultBranch: opts.defaultBranch,
288
- prNumber: opts.prNumber, status: opts.status,
289
- limit: opts.limit, offset: opts.offset,
290
- }));
291
- });
292
-
293
- codeReview.command('get')
294
- .description('Get detailed information for a specific code review')
295
- .option('--name <repo>', 'Repository (owner/repo)')
296
- .option('--remote <provider>', 'github, gitlab, bitbucket, azure')
297
- .requiredOption('--pr-number <n>', 'PR number', parseInt)
298
- .requiredOption('--review-id <id>', 'Code review ID')
299
- .action((options) => {
300
- const opts = resolveRepoOpts(options);
301
- runCmd(() => scm.getCodeReview({
302
- name: opts.name, remote: opts.remote,
303
- prNumber: opts.prNumber, reviewId: opts.reviewId,
304
- }));
305
- });
306
-
307
- // ─── Comment search ───
308
- const comments = program.command('comments').description('Comment search tools');
309
-
310
- comments.command('search')
311
- .description('Search across all CodeAnt comments')
312
- .option('--name <repo>', 'Repository (owner/repo)')
313
- .option('--remote <provider>', 'github, gitlab, bitbucket, azure')
314
- .requiredOption('--query <term>', 'Search term')
315
- .option('--limit <n>', 'Max results (default: 10, max: 50)', parseInt, 10)
316
- .option('--include-addressed', 'Include resolved comments (default: false)')
317
- .option('--created-after <date>', 'ISO 8601 date filter')
318
- .action((options) => {
319
- const opts = resolveRepoOpts(options);
320
- runCmd(() => scm.searchComments({
321
- name: opts.name, remote: opts.remote,
322
- query: opts.query, limit: opts.limit,
323
- includeAddressed: opts.includeAddressed || false,
324
- createdAfter: opts.createdAfter,
325
- }));
326
- });
327
-
328
- // ─── Analytics tracking (for external callers like Claude Code skills) ───
329
- program
330
- .command('track')
331
- .description('Send an analytics event')
332
- .requiredOption('--event <name>', 'Event name (e.g. skill_invoked, suggestions_applied)')
333
- .option('--props <json>', 'JSON string of event properties', '{}')
334
- .action(async (options) => {
335
- try {
336
- const props = JSON.parse(options.props);
337
- if (typeof props !== 'object' || props === null || Array.isArray(props)) {
338
- process.stderr.write('--props must be a JSON object\n');
339
- return;
340
- }
341
- track(options.event, props);
342
- await analyticsShutdown();
343
- } catch (err) {
344
- process.stderr.write(`Invalid --props JSON: ${err instanceof Error ? err.message : String(err)}\n`);
345
- }
346
- });
347
-
348
- program.parse();
349
- }
@@ -1,198 +0,0 @@
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
- }
@@ -1,21 +0,0 @@
1
- import * as azdev from 'azure-devops-node-api';
2
- import { getAzureDevOpsToken } from '../../utils/scmAuth.js';
3
- import { getConfigValue } from '../../utils/config.js';
4
-
5
- export function getConnection() {
6
- const token = getAzureDevOpsToken();
7
- if (!token) throw new Error('Azure DevOps token not found. Set AZURE_DEVOPS_TOKEN env var or run `codeant set-token azure <token>`.');
8
-
9
- const orgUrl = process.env.AZURE_DEVOPS_ORG_URL || getConfigValue('azureDevOpsOrgUrl');
10
- if (!orgUrl) throw new Error('Azure DevOps org URL not found. Set AZURE_DEVOPS_ORG_URL env var or run `codeant set-azure-org <url>`.');
11
-
12
- const authHandler = azdev.getPersonalAccessTokenHandler(token);
13
- return new azdev.WebApi(orgUrl, authHandler);
14
- }
15
-
16
- // Azure repos use "project/repo" format
17
- export function splitRepo(name) {
18
- const [project, repo] = name.split('/');
19
- if (!project || !repo) throw new Error(`Invalid repo name "${name}". Use project/repo format.`);
20
- return { project, repo };
21
- }
@@ -1,20 +0,0 @@
1
- import { getConnection, splitRepo } from './client.js';
2
-
3
- export async function getCodeReview({ name, prNumber, reviewId }) {
4
- const connection = getConnection();
5
- const gitApi = await connection.getGitApi();
6
- const { project, repo } = splitRepo(name);
7
-
8
- const pr = await gitApi.getPullRequest(repo, prNumber, project);
9
- const reviewer = (pr.reviewers || []).find(r => String(r.id) === String(reviewId));
10
-
11
- if (!reviewer) throw new Error(`Reviewer ${reviewId} not found on PR #${prNumber}`);
12
-
13
- return {
14
- id: reviewer.id,
15
- user: reviewer.displayName || reviewer.uniqueName,
16
- state: reviewer.vote > 0 ? 'APPROVED' : reviewer.vote < 0 ? 'REJECTED' : 'PENDING',
17
- body: null,
18
- comments: [],
19
- };
20
- }
@@ -1,30 +0,0 @@
1
- import { getConnection, splitRepo } from './client.js';
2
-
3
- export async function getPullRequest({ name, prNumber }) {
4
- const connection = getConnection();
5
- const gitApi = await connection.getGitApi();
6
- const { project, repo } = splitRepo(name);
7
-
8
- const pr = await gitApi.getPullRequest(repo, prNumber, project);
9
- const threads = await gitApi.getThreads(repo, prNumber, project);
10
-
11
- const reviewers = (pr.reviewers || []).map(r => ({
12
- user: r.displayName || r.uniqueName,
13
- state: r.vote > 0 ? 'APPROVED' : r.vote < 0 ? 'REJECTED' : 'PENDING',
14
- }));
15
-
16
- return {
17
- number: pr.pullRequestId,
18
- title: pr.title,
19
- state: pr.status === 1 ? 'open' : 'closed',
20
- merged: pr.status === 3,
21
- author: pr.createdBy?.displayName || pr.createdBy?.uniqueName,
22
- sourceBranch: pr.sourceRefName?.replace('refs/heads/', ''),
23
- targetBranch: pr.targetRefName?.replace('refs/heads/', ''),
24
- body: pr.description,
25
- createdAt: pr.creationDate,
26
- url: pr.url,
27
- reviewSummary: reviewers,
28
- threadCount: threads?.length || 0,
29
- };
30
- }
@@ -1,6 +0,0 @@
1
- export { listPullRequests } from './listPullRequests.js';
2
- export { getPullRequest } from './getPullRequest.js';
3
- export { listPullRequestComments } from './listPullRequestComments.js';
4
- export { listCodeReviews } from './listCodeReviews.js';
5
- export { getCodeReview } from './getCodeReview.js';
6
- export { searchComments } from './searchComments.js';
@@ -1,17 +0,0 @@
1
- import { getConnection, splitRepo } from './client.js';
2
-
3
- export async function listCodeReviews({ name, prNumber, limit = 20 }) {
4
- const connection = getConnection();
5
- const gitApi = await connection.getGitApi();
6
- const { project, repo } = splitRepo(name);
7
-
8
- const pr = await gitApi.getPullRequest(repo, prNumber, project);
9
-
10
- return (pr.reviewers || []).slice(0, limit).map(r => ({
11
- id: r.id,
12
- user: r.displayName || r.uniqueName,
13
- state: r.vote > 0 ? 'APPROVED' : r.vote < 0 ? 'REJECTED' : 'PENDING',
14
- body: null,
15
- submittedAt: null,
16
- }));
17
- }