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
@@ -1,275 +0,0 @@
1
- import CommonApiHelper from './commonApiHelper.js';
2
- import path from 'path';
3
- import fs from 'fs';
4
- import { minimatch } from 'minimatch';
5
-
6
- const MAX_REVIEW_FILES = 10;
7
- const MAX_FILE_LINES = 5000;
8
-
9
- const EXCLUDED_FILENAMES = new Set([
10
- 'package-lock.json',
11
- 'yarn.lock',
12
- 'pnpm-lock.yaml',
13
- 'composer.lock',
14
- 'Gemfile.lock',
15
- 'Cargo.lock',
16
- 'poetry.lock',
17
- 'Pipfile.lock',
18
- 'go.sum',
19
- '.DS_Store',
20
- 'Thumbs.db',
21
- ]);
22
-
23
- const EXCLUDED_EXTENSIONS = new Set([
24
- // Images
25
- '.png', '.jpg', '.jpeg', '.gif', '.bmp', '.ico', '.svg', '.webp', '.avif',
26
- // Fonts
27
- '.woff', '.woff2', '.ttf', '.eot', '.otf',
28
- // Binary / compiled
29
- '.exe', '.dll', '.so', '.dylib', '.bin', '.o', '.a', '.class', '.pyc', '.pyo',
30
- '.wasm', '.jar', '.war', '.ear',
31
- // Archives
32
- '.zip', '.tar', '.gz', '.bz2', '.7z', '.rar',
33
- // Media
34
- '.mp3', '.mp4', '.wav', '.avi', '.mov', '.flv', '.wmv', '.ogg', '.webm',
35
- // Data / generated
36
- '.min.js', '.min.css', '.map',
37
- '.pb', '.proto.bin',
38
- // PDF / docs
39
- '.pdf', '.doc', '.docx', '.xls', '.xlsx', '.ppt', '.pptx',
40
- ]);
41
-
42
- function matchesPatterns(filePath, includePatterns, excludePatterns) {
43
- if (includePatterns.length > 0) {
44
- const included = includePatterns.some(pattern => minimatch(filePath, pattern, { matchBase: true }));
45
- if (!included) return false;
46
- }
47
- if (excludePatterns.length > 0) {
48
- const excluded = excludePatterns.some(pattern => minimatch(filePath, pattern, { matchBase: true }));
49
- if (excluded) return false;
50
- }
51
- return true;
52
- }
53
-
54
- function isReviewableFile(filePath) {
55
- const basename = path.basename(filePath);
56
-
57
- if (EXCLUDED_FILENAMES.has(basename)) return false;
58
-
59
- const ext = path.extname(basename).toLowerCase();
60
- if (EXCLUDED_EXTENSIONS.has(ext)) return false;
61
-
62
- // Check for .min.js / .min.css (double extension)
63
- if (basename.endsWith('.min.js') || basename.endsWith('.min.css')) return false;
64
-
65
- return true;
66
- }
67
-
68
- /**
69
- * Transforms git diff data into the format expected by the agentic review API
70
- */
71
- class ReviewApiHelper extends CommonApiHelper {
72
- /**
73
- * Pass structured diffs through so buildReviewApiRequest can filter them
74
- */
75
- _transformDiffsToApiFormat(diffs) {
76
- return diffs;
77
- }
78
-
79
- /**
80
- * Build the complete request body for the agentic review API
81
- */
82
- async buildReviewApiRequest(type = 'staged-only', includePatterns = [], excludePatterns = [], options = {}) {
83
- const diffs = await this.getFilesForType(type, options);
84
-
85
- // diffs is either a string (from getAllDiffs) or an array of structured diff objects
86
- if (typeof diffs === 'string') {
87
- // Split the raw diff into per-file sections and filter out deleted files
88
- const gitRoot = this.getGitRoot() || process.cwd();
89
- const fileContents = {};
90
- const fileSections = diffs.split(/(?=^diff --git )/m).filter(Boolean);
91
- const keptSections = [];
92
- const skipped = [];
93
-
94
- for (const section of fileSections) {
95
- const nameMatch = section.match(/^diff --git a\/.+ b\/(.+)$/m);
96
- if (!nameMatch) continue;
97
- const f = nameMatch[1];
98
- if (!isReviewableFile(f)) {
99
- skipped.push({ file: f, reason: 'binary/generated' });
100
- continue;
101
- }
102
- // Apply user-specified --include/--exclude glob patterns
103
- if (!matchesPatterns(f, includePatterns, excludePatterns)) {
104
- skipped.push({ file: f, reason: 'excluded by pattern' });
105
- continue;
106
- }
107
-
108
- // Skip deleted files — no file content to review, diff alone isn't actionable
109
- if (/deleted file mode/m.test(section) || /\+\+\+ \/dev\/null/m.test(section)) {
110
- skipped.push({ file: f, reason: 'deleted' });
111
- continue;
112
- }
113
-
114
- // Skip files exceeding line limit to avoid blowing up the AI context window
115
- try {
116
- const fullPath = path.resolve(gitRoot, f);
117
- if (fs.existsSync(fullPath)) {
118
- const content = fs.readFileSync(fullPath, 'utf-8');
119
- const lineCount = content.split('\n').length;
120
- if (lineCount > MAX_FILE_LINES) {
121
- skipped.push({ file: f, reason: `too large (${lineCount} lines, max ${MAX_FILE_LINES})` });
122
- continue;
123
- }
124
- keptSections.push(section);
125
- fileContents[f] = content;
126
- } else {
127
- keptSections.push(section);
128
- }
129
- } catch {
130
- // Skip files that can't be read
131
- }
132
- }
133
-
134
- const reviewedFiles = Object.keys(fileContents);
135
- return {
136
- diff_content: keptSections.join(''),
137
- file_contents: fileContents,
138
- _meta: {
139
- reviewed_files: reviewedFiles,
140
- total_changed: fileSections.length,
141
- skipped,
142
- capped: false,
143
- max_files: MAX_REVIEW_FILES,
144
- },
145
- };
146
- }
147
-
148
- // Filter and track skipped files
149
- const skipped = [];
150
- const allUniqueFiles = new Set(diffs.map(d => d.filename_str));
151
- const reviewable = [];
152
-
153
- for (const d of diffs) {
154
- if (!isReviewableFile(d.filename_str)) {
155
- skipped.push({ file: d.filename_str, reason: 'binary/generated' });
156
- continue;
157
- }
158
- if (d.edit_type_str === 'DELETED') {
159
- skipped.push({ file: d.filename_str, reason: 'deleted' });
160
- continue;
161
- }
162
- if (!matchesPatterns(d.filename_str, includePatterns, excludePatterns)) {
163
- skipped.push({ file: d.filename_str, reason: 'excluded by pattern' });
164
- continue;
165
- }
166
- reviewable.push(d);
167
- }
168
-
169
- // Dedupe skipped entries
170
- const seenSkipped = new Set();
171
- const dedupedSkipped = skipped.filter(s => {
172
- if (seenSkipped.has(s.file)) return false;
173
- seenSkipped.add(s.file);
174
- return true;
175
- });
176
-
177
- // Dedupe by filename, keeping all hunks, but limit unique files to MAX_REVIEW_FILES
178
- const fileOrder = [];
179
- const fileHunks = new Map();
180
- let capped = false;
181
- for (const d of reviewable) {
182
- const f = d.filename_str;
183
- if (!fileHunks.has(f)) {
184
- if (fileOrder.length >= MAX_REVIEW_FILES) {
185
- capped = true;
186
- if (!seenSkipped.has(f)) {
187
- dedupedSkipped.push({ file: f, reason: `exceeded ${MAX_REVIEW_FILES}-file limit` });
188
- seenSkipped.add(f);
189
- }
190
- continue;
191
- }
192
- fileOrder.push(f);
193
- fileHunks.set(f, []);
194
- }
195
- fileHunks.get(f).push(d);
196
- }
197
-
198
- // Reconstruct a combined diff string from the filtered hunks
199
- const parts = [];
200
- for (const f of fileOrder) {
201
- const hunks = fileHunks.get(f);
202
- // Build a file-level diff header + all hunk patches
203
- const header = `diff --git a/${f} b/${f}\n--- a/${f}\n+++ b/${f}`;
204
- const hunkPatches = hunks.map(h => h.patch_str).filter(Boolean);
205
- if (hunkPatches.length > 0) {
206
- parts.push(header + '\n' + hunkPatches.join('\n'));
207
- }
208
- }
209
-
210
- // Use head_file_str from diffs so file content matches the diff version (avoids working tree mismatch for --last-commit etc.)
211
- const fileContents = {};
212
- const skippedLargeFiles = new Set();
213
- for (const f of fileOrder) {
214
- const hunks = fileHunks.get(f);
215
- const content = hunks[0]?.head_file_str || '';
216
- if (content) {
217
- const lineCount = content.split('\n').length;
218
- if (lineCount > MAX_FILE_LINES) {
219
- skippedLargeFiles.add(f);
220
- dedupedSkipped.push({ file: f, reason: `too large (${lineCount} lines, max ${MAX_FILE_LINES})` });
221
- continue;
222
- }
223
- fileContents[f] = content;
224
- }
225
- }
226
-
227
- // Remove diff sections for skipped files
228
- const filteredParts = parts.filter((_, i) => !skippedLargeFiles.has(fileOrder[i]));
229
-
230
- const reviewedFiles = fileOrder.filter(f => !skippedLargeFiles.has(f));
231
- return {
232
- diff_content: filteredParts.join('\n'),
233
- file_contents: fileContents,
234
- _meta: {
235
- reviewed_files: reviewedFiles,
236
- total_changed: allUniqueFiles.size,
237
- skipped: dedupedSkipped,
238
- capped,
239
- max_files: MAX_REVIEW_FILES,
240
- },
241
- };
242
- }
243
-
244
- /**
245
- * Split a combined review request into per-file payloads.
246
- * Each payload has a single file's diff and content.
247
- */
248
- static splitIntoPerFileRequests(requestBody) {
249
- if (!requestBody?.diff_content?.length) return [];
250
-
251
- const fileSections = requestBody.diff_content.split(/(?=^diff --git )/m).filter(Boolean);
252
- const perFileRequests = [];
253
-
254
- for (const section of fileSections) {
255
- const nameMatch = section.match(/^diff --git a\/.+ b\/(.+)$/m);
256
- if (!nameMatch) continue;
257
- const filename = nameMatch[1];
258
-
259
- const fileContents = {};
260
- if (requestBody.file_contents?.[filename]) {
261
- fileContents[filename] = requestBody.file_contents[filename];
262
- }
263
-
264
- perFileRequests.push({
265
- diff_content: section,
266
- file_contents: fileContents,
267
- _filename: filename,
268
- });
269
- }
270
-
271
- return perFileRequests;
272
- }
273
- }
274
-
275
- export default ReviewApiHelper;
@@ -1,136 +0,0 @@
1
- import { execSync } from 'child_process';
2
- import { getConfigValue } from './config.js';
3
-
4
- /**
5
- * Resolves auth tokens and base URLs for each SCM platform.
6
- * Priority: env var → platform CLI → ~/.codeant/config.json → auto-detect from git origin
7
- */
8
-
9
- function tryExec(cmd) {
10
- try {
11
- return execSync(cmd, { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
12
- } catch {
13
- return null;
14
- }
15
- }
16
-
17
- // ─── Auto-detect host from git remote origin ───
18
-
19
- const PUBLIC_HOSTS = {
20
- 'github.com': 'github',
21
- 'gitlab.com': 'gitlab',
22
- 'bitbucket.org': 'bitbucket',
23
- 'dev.azure.com': 'azure',
24
- 'visualstudio.com': 'azure',
25
- };
26
-
27
- let _originInfo = undefined; // cached
28
-
29
- function getOriginInfo() {
30
- if (_originInfo !== undefined) return _originInfo;
31
- _originInfo = null;
32
- try {
33
- const origin = execSync('git remote get-url origin', { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
34
-
35
- // Parse host from SSH (git@host:owner/repo) or HTTPS (https://host/owner/repo)
36
- let host = null;
37
- const sshMatch = origin.match(/^[^@]+@([^:]+):/);
38
- const httpsMatch = origin.match(/^https?:\/\/([^/]+)/);
39
- if (sshMatch) host = sshMatch[1];
40
- else if (httpsMatch) host = httpsMatch[1];
41
-
42
- if (!host) return _originInfo;
43
-
44
- // Determine platform from host
45
- let platform = null;
46
- for (const [publicHost, p] of Object.entries(PUBLIC_HOSTS)) {
47
- if (host === publicHost || host.endsWith('.' + publicHost)) {
48
- platform = p;
49
- break;
50
- }
51
- }
52
- // Heuristic for self-hosted: check if hostname contains platform name
53
- if (!platform) {
54
- if (host.includes('github')) platform = 'github';
55
- else if (host.includes('gitlab')) platform = 'gitlab';
56
- else if (host.includes('bitbucket')) platform = 'bitbucket';
57
- else if (host.includes('azure') || host.includes('visualstudio')) platform = 'azure';
58
- }
59
-
60
- const isPublic = Object.keys(PUBLIC_HOSTS).some(h => host === h);
61
- const baseUrl = isPublic ? null : `https://${host}`;
62
-
63
- _originInfo = { host, platform, baseUrl, origin };
64
- } catch {}
65
- return _originInfo;
66
- }
67
-
68
- function detectBaseUrlForPlatform(platform) {
69
- const info = getOriginInfo();
70
- if (info && info.platform === platform && info.baseUrl) return info.baseUrl;
71
- return null;
72
- }
73
-
74
- // ─── Token resolvers ───
75
-
76
- export function getGitHubToken() {
77
- return process.env.GITHUB_TOKEN
78
- || process.env.GH_TOKEN
79
- || tryExec('gh auth token')
80
- || getConfigValue('githubToken')
81
- || null;
82
- }
83
-
84
- export function getGitLabToken() {
85
- return process.env.GITLAB_TOKEN
86
- || tryExec('glab auth token 2>/dev/null')
87
- || getConfigValue('gitlabToken')
88
- || null;
89
- }
90
-
91
- export function getBitbucketToken() {
92
- return process.env.BITBUCKET_TOKEN
93
- || getConfigValue('bitbucketToken')
94
- || null;
95
- }
96
-
97
- export function getAzureDevOpsToken() {
98
- return process.env.AZURE_DEVOPS_TOKEN
99
- || process.env.AZURE_DEVOPS_PAT
100
- || getConfigValue('azureDevOpsToken')
101
- || null;
102
- }
103
-
104
- export function getTokenForRemote(remote) {
105
- switch (remote) {
106
- case 'github': return getGitHubToken();
107
- case 'gitlab': return getGitLabToken();
108
- case 'bitbucket': return getBitbucketToken();
109
- case 'azure': return getAzureDevOpsToken();
110
- default: throw new Error(`Unknown remote: ${remote}. Use: github, gitlab, bitbucket, azure`);
111
- }
112
- }
113
-
114
- // ─── Base URL resolvers for self-hosted instances ───
115
- // Priority: env var → config → auto-detect from git origin
116
-
117
- export function getGitHubBaseUrl() {
118
- return process.env.GITHUB_API_URL
119
- || process.env.GH_ENTERPRISE_URL
120
- || getConfigValue('githubBaseUrl')
121
- || detectBaseUrlForPlatform('github');
122
- }
123
-
124
- export function getGitLabBaseUrl() {
125
- return process.env.GITLAB_URL
126
- || process.env.GITLAB_HOST
127
- || getConfigValue('gitlabBaseUrl')
128
- || detectBaseUrlForPlatform('gitlab');
129
- }
130
-
131
- export function getBitbucketBaseUrl() {
132
- return process.env.BITBUCKET_URL
133
- || process.env.BITBUCKET_SERVER_URL
134
- || getConfigValue('bitbucketBaseUrl')
135
- || detectBaseUrlForPlatform('bitbucket');
136
- }
@@ -1,242 +0,0 @@
1
- import CommonApiHelper from './commonApiHelper.js';
2
- import path from 'path';
3
- import { minimatch } from 'minimatch';
4
-
5
- /**
6
- * Transforms git diff data into the format expected by the secrets detection API
7
- *
8
- * API Input Format:
9
- * {
10
- * "files": [
11
- * {
12
- * "file_path": str,
13
- * "code": str,
14
- * "diffs": [{ "start_line": int, "end_line": int }]
15
- * }
16
- * ]
17
- * }
18
- */
19
-
20
- const EXCLUDED_FILENAMES = new Set([
21
- 'package-lock.json',
22
- 'yarn.lock',
23
- 'pnpm-lock.yaml',
24
- 'composer.lock',
25
- 'Gemfile.lock',
26
- 'Cargo.lock',
27
- 'poetry.lock',
28
- 'Pipfile.lock',
29
- 'go.sum',
30
- '.DS_Store',
31
- 'Thumbs.db',
32
- ]);
33
-
34
- const EXCLUDED_EXTENSIONS = new Set([
35
- // Images
36
- '.png', '.jpg', '.jpeg', '.gif', '.bmp', '.ico', '.svg', '.webp', '.avif',
37
- // Fonts
38
- '.woff', '.woff2', '.ttf', '.eot', '.otf',
39
- // Binary / compiled
40
- '.exe', '.dll', '.so', '.dylib', '.bin', '.o', '.a', '.class', '.pyc', '.pyo',
41
- '.wasm', '.jar', '.war', '.ear',
42
- // Archives
43
- '.zip', '.tar', '.gz', '.bz2', '.7z', '.rar',
44
- // Media
45
- '.mp3', '.mp4', '.wav', '.avi', '.mov', '.flv', '.wmv', '.ogg', '.webm',
46
- // Data / generated
47
- '.min.js', '.min.css', '.map',
48
- '.pb', '.proto.bin',
49
- // PDF / docs
50
- '.pdf', '.doc', '.docx', '.xls', '.xlsx', '.ppt', '.pptx',
51
- ]);
52
-
53
- const MAX_FILE_LINES = 5000;
54
-
55
- function isScannableFile(filePath) {
56
- const basename = path.basename(filePath);
57
-
58
- if (EXCLUDED_FILENAMES.has(basename)) return false;
59
-
60
- const ext = path.extname(basename).toLowerCase();
61
- if (EXCLUDED_EXTENSIONS.has(ext)) return false;
62
-
63
- if (basename.endsWith('.min.js') || basename.endsWith('.min.css')) return false;
64
-
65
- // Skip markdown files
66
- if (ext === '.md') return false;
67
-
68
- return true;
69
- }
70
-
71
- class SecretsApiHelper extends CommonApiHelper {
72
- /**
73
- * Transform diff info array to API format
74
- * Groups by file and extracts diff ranges
75
- */
76
- _transformDiffsToApiFormat(diffs) {
77
- // Group diffs by filename
78
- const fileMap = new Map();
79
-
80
- for (const diff of diffs) {
81
- const filePath = diff.filename_str;
82
-
83
- // Skip diffs with missing or invalid filename
84
- if (!filePath) {
85
- continue;
86
- }
87
-
88
- if (!fileMap.has(filePath)) {
89
- fileMap.set(filePath, {
90
- file_path: filePath,
91
- code: diff.head_file_str || '',
92
- diffs: []
93
- });
94
- }
95
-
96
- // Add diff range if available
97
- if (diff.start_line_str && diff.end_line_str) {
98
- const startLine = parseInt(diff.start_line_str, 10);
99
- const endLine = parseInt(diff.end_line_str, 10);
100
-
101
- if (!isNaN(startLine) && !isNaN(endLine)) {
102
- fileMap.get(filePath).diffs.push({
103
- start_line: startLine,
104
- end_line: endLine
105
- });
106
- }
107
- }
108
- }
109
-
110
- return Array.from(fileMap.values());
111
- }
112
-
113
- /**
114
- * Filter files based on include and exclude glob patterns
115
- * @param {Array} files - Array of file objects with 'file_path' property
116
- * @param {Array} includePatterns - Array of glob pattern strings to include
117
- * @param {Array} excludePatterns - Array of glob pattern strings to exclude
118
- */
119
- _filterFiles(files, includePatterns = [], excludePatterns = []) {
120
- return files.filter(fileObj => {
121
- const filePath = fileObj.file_path;
122
-
123
- // If include patterns are specified, file must match at least one
124
- if (includePatterns.length > 0) {
125
- const matchesInclude = includePatterns.some(pattern => {
126
- try {
127
- if (pattern instanceof RegExp) {
128
- return pattern.test(filePath);
129
- }
130
- return minimatch(filePath, pattern, { matchBase: true });
131
- } catch (e) {
132
- console.warn(`Invalid include pattern: ${pattern}`, e.message);
133
- return false;
134
- }
135
- });
136
-
137
- if (!matchesInclude) {
138
- return false;
139
- }
140
- }
141
-
142
- // If exclude patterns are specified, file must not match any
143
- if (excludePatterns.length > 0) {
144
- const matchesExclude = excludePatterns.some(pattern => {
145
- try {
146
- if (pattern instanceof RegExp) {
147
- return pattern.test(filePath);
148
- }
149
- return minimatch(filePath, pattern, { matchBase: true });
150
- } catch (e) {
151
- console.warn(`Invalid exclude pattern: ${pattern}`, e.message);
152
- return false;
153
- }
154
- });
155
-
156
- if (matchesExclude) {
157
- return false;
158
- }
159
- }
160
-
161
- return true;
162
- });
163
- }
164
-
165
- /**
166
- * Build the complete request body for the secrets API
167
- * @param {string} type - Type of scan (staged-only, branch-diff, etc.)
168
- * @param {Array} includePatterns - Optional array of glob patterns to include files
169
- * @param {Array} excludePatterns - Optional array of glob patterns to exclude files
170
- * @param {Object} options - Additional options like lastNCommits
171
- */
172
- async buildSecretsApiRequest(type = 'staged-only', includePatterns = [], excludePatterns = [], options = {}) {
173
- let files = await this.getFilesForType(type, options);
174
-
175
- // Handle null/undefined return
176
- if (!files || !Array.isArray(files)) {
177
- files = [];
178
- }
179
-
180
- const totalChanged = files.length;
181
- const skipped = [];
182
-
183
- // Filter out non-scannable files (binary, lock files, markdown, etc.)
184
- files = files.filter(fileObj => {
185
- const filePath = fileObj.file_path;
186
-
187
- if (!isScannableFile(filePath)) {
188
- skipped.push({ file: filePath, reason: 'binary/generated' });
189
- return false;
190
- }
191
-
192
- // Skip test files — backend filters these out anyway
193
- if (filePath.toLowerCase().includes('test')) {
194
- skipped.push({ file: filePath, reason: 'test file' });
195
- return false;
196
- }
197
-
198
- // Skip deleted files (empty code)
199
- if (fileObj.code === '' || fileObj.code === undefined) {
200
- skipped.push({ file: filePath, reason: 'deleted' });
201
- return false;
202
- }
203
-
204
- // Skip files exceeding line limit
205
- if (fileObj.code) {
206
- const lineCount = fileObj.code.split('\n').length;
207
- if (lineCount > MAX_FILE_LINES) {
208
- skipped.push({ file: filePath, reason: `too large (${lineCount} lines, max ${MAX_FILE_LINES})` });
209
- return false;
210
- }
211
- }
212
-
213
- return true;
214
- });
215
-
216
- // Apply include/exclude filters
217
- const beforePatternFilter = files.length;
218
- files = this._filterFiles(files, includePatterns, excludePatterns);
219
-
220
- // Track pattern-excluded files
221
- if (files.length < beforePatternFilter) {
222
- // We don't have exact file names for pattern-excluded, but the count is in skipped
223
- const patternExcluded = beforePatternFilter - files.length;
224
- if (patternExcluded > 0) {
225
- skipped.push({ file: `${patternExcluded} file(s)`, reason: 'excluded by pattern' });
226
- }
227
- }
228
-
229
- const scannedFiles = files.map(f => f.file_path);
230
-
231
- return {
232
- files,
233
- _meta: {
234
- scanned_files: scannedFiles,
235
- total_changed: totalChanged,
236
- skipped,
237
- },
238
- };
239
- }
240
- }
241
-
242
- export default SecretsApiHelper;