codeant-cli 0.1.1 → 0.1.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.
@@ -0,0 +1,405 @@
1
+ import React, { useEffect, useState } from 'react';
2
+ import { Box, Text, useApp } from 'ink';
3
+ import { getConfigValue } from '../utils/config.js';
4
+ import { fetchApi } from '../utils/fetchApi.js';
5
+ import StaticAnalysisApiHelper from '../utils/staticAnalysisApiHelper.js';
6
+ import fs from 'fs/promises';
7
+ import path from 'path';
8
+
9
+ export default function StaticAnalysis({ scanType = 'staged-only', failOn = 'CRITICAL', include = [], exclude = [], autoFix = false }) {
10
+ const { exit } = useApp();
11
+ const [status, setStatus] = useState('initializing');
12
+ const [issues, setIssues] = useState([]);
13
+ const [error, setError] = useState(null);
14
+ const [fileCount, setFileCount] = useState(0);
15
+ const [fixedFiles, setFixedFiles] = useState([]);
16
+ const apiKey = getConfigValue('apiKey');
17
+
18
+ // Handle not logged in state
19
+ useEffect(() => {
20
+ if (!apiKey) {
21
+ const id = setTimeout(() => exit(new Error('Not logged in')), 0);
22
+ return () => clearTimeout(id);
23
+ }
24
+ }, [apiKey, exit]);
25
+
26
+ // Check if logged in
27
+ if (!apiKey) {
28
+ return React.createElement(
29
+ Box,
30
+ { flexDirection: 'column', padding: 1 },
31
+ React.createElement(Text, { color: 'red' }, '✗ Not logged in.'),
32
+ React.createElement(Text, { color: 'gray' }, 'Run "codeant login" to authenticate.'),
33
+ );
34
+ }
35
+
36
+ // Helper to check if an issue should cause failure based on failOn level
37
+ const shouldFailOn = (issueType) => {
38
+ const type = issueType?.toUpperCase();
39
+ const failOnUpper = failOn?.toUpperCase();
40
+ if (!failOnUpper || failOnUpper === 'NONE') return false;
41
+ if (failOnUpper === 'BLOCKER') return type === 'BLOCKER';
42
+ if (failOnUpper === 'CRITICAL') return type === 'BLOCKER' || type === 'CRITICAL';
43
+ if (failOnUpper === 'MAJOR') return type === 'BLOCKER' || type === 'CRITICAL' || type === 'MAJOR';
44
+ if (failOnUpper === 'MINOR') return type === 'BLOCKER' || type === 'CRITICAL' || type === 'MAJOR' || type === 'MINOR';
45
+ return true; // 'INFO' or 'ALL' - fail on any issue
46
+ };
47
+
48
+ useEffect(() => {
49
+ let cancelled = false;
50
+
51
+ async function scanCode() {
52
+ try {
53
+ if (cancelled) return;
54
+ setStatus('scanning');
55
+
56
+ // Initialize git helper and get files
57
+ const helper = new StaticAnalysisApiHelper(process.cwd());
58
+ await helper.init();
59
+
60
+ if (cancelled) return;
61
+ const requestBody = await helper.buildStaticAnalysisApiRequest(scanType, include, exclude);
62
+ setFileCount(requestBody.filesData.length);
63
+
64
+ if (requestBody.filesData.length === 0) {
65
+ if (!cancelled) setStatus('no_files');
66
+ return;
67
+ }
68
+
69
+ // Call the static analysis API
70
+ const response = await fetchApi(
71
+ '/extension/prReview/static-analysis',
72
+ 'POST',
73
+ requestBody,
74
+ );
75
+
76
+ if (cancelled) return;
77
+ const codeSuggestions = response.codeSuggestions || [];
78
+
79
+ // Filter to only include files with actual issues
80
+ const filesWithIssues = codeSuggestions.filter(
81
+ file => file.issues && file.issues.length > 0,
82
+ );
83
+
84
+ // Auto-fix issues if enabled
85
+ const fixedFilesList = [];
86
+ if (autoFix && filesWithIssues.length > 0) {
87
+ const gitRoot = helper.getGitRoot();
88
+
89
+ for (const fileData of filesWithIssues) {
90
+ const fixableIssues = fileData.issues.filter(
91
+ issue => issue.fixAvailable && issue.fix?.inputFileEdits?.length > 0
92
+ );
93
+
94
+ if (fixableIssues.length > 0) {
95
+ try {
96
+ const filePath = path.join(gitRoot, fileData.file);
97
+ let fileContent = await fs.readFile(filePath, 'utf8');
98
+ const lines = fileContent.split('\n');
99
+
100
+ // Collect all text edits from all issues
101
+ const allEdits = [];
102
+ for (const issue of fixableIssues) {
103
+ for (const fileEdit of issue.fix.inputFileEdits) {
104
+ if (fileEdit.target === fileData.file) {
105
+ for (const textEdit of fileEdit.textEdits) {
106
+ allEdits.push({
107
+ textRange: textEdit.textRange,
108
+ newText: textEdit.newText,
109
+ });
110
+ }
111
+ }
112
+ }
113
+ }
114
+
115
+ // Sort edits by position (descending) to avoid offset issues
116
+ allEdits.sort((a, b) => {
117
+ if (b.textRange.startLine !== a.textRange.startLine) {
118
+ return b.textRange.startLine - a.textRange.startLine;
119
+ }
120
+ if (b.textRange.startLineOffset !== a.textRange.startLineOffset) {
121
+ return b.textRange.startLineOffset - a.textRange.startLineOffset;
122
+ }
123
+ if (b.textRange.endLine !== a.textRange.endLine) {
124
+ return b.textRange.endLine - a.textRange.endLine;
125
+ }
126
+ return b.textRange.endLineOffset - a.textRange.endLineOffset;
127
+ });
128
+
129
+ // Apply each edit
130
+ for (const edit of allEdits) {
131
+ const { startLine, startLineOffset, endLine, endLineOffset } = edit.textRange;
132
+
133
+ if (startLine === endLine) {
134
+ // Single line edit
135
+ const line = lines[startLine - 1];
136
+ lines[startLine - 1] =
137
+ line.substring(0, startLineOffset) +
138
+ edit.newText +
139
+ line.substring(endLineOffset);
140
+ } else {
141
+ // Multi-line edit
142
+ const firstLine = lines[startLine - 1];
143
+ const lastLine = lines[endLine - 1];
144
+
145
+ const newContent =
146
+ firstLine.substring(0, startLineOffset) +
147
+ edit.newText +
148
+ lastLine.substring(endLineOffset);
149
+
150
+ // Replace the lines
151
+ lines.splice(startLine - 1, endLine - startLine + 1, newContent);
152
+ }
153
+ }
154
+
155
+ await fs.writeFile(filePath, lines.join('\n'), 'utf8');
156
+ fixedFilesList.push({
157
+ file: fileData.file,
158
+ fixedCount: fixableIssues.length,
159
+ });
160
+ } catch (err) {
161
+ console.error(`Failed to fix ${fileData.file}: ${err.message}`);
162
+ }
163
+ }
164
+ }
165
+ }
166
+
167
+ if (!cancelled) {
168
+ setFixedFiles(fixedFilesList);
169
+ setIssues(filesWithIssues);
170
+ setStatus('done');
171
+ }
172
+ } catch (err) {
173
+ if (!cancelled) {
174
+ setError(err.message);
175
+ setStatus('error');
176
+ }
177
+ }
178
+ }
179
+
180
+ scanCode();
181
+
182
+ return () => {
183
+ cancelled = true;
184
+ };
185
+ }, [scanType, include, exclude, autoFix]);
186
+
187
+ // Handle exit after status changes
188
+ useEffect(() => {
189
+ if (status === 'done') {
190
+ // Check if any issues should cause failure based on failOn level
191
+ const hasBlockingIssues = issues.some(file =>
192
+ file.issues.some(issue => shouldFailOn(issue.type)),
193
+ );
194
+
195
+ if (hasBlockingIssues) {
196
+ setTimeout(() => {
197
+ process.exitCode = 1;
198
+ exit(new Error('Code issues detected'));
199
+ }, 100);
200
+ } else {
201
+ setTimeout(() => exit(), 100);
202
+ }
203
+ } else if (status === 'no_files') {
204
+ setTimeout(() => exit(), 100);
205
+ } else if (status === 'error') {
206
+ setTimeout(() => exit(new Error(error)), 100);
207
+ }
208
+ }, [status, issues, failOn, exit, shouldFailOn, error]);
209
+
210
+ // Render: Initializing
211
+ if (status === 'initializing') {
212
+ return React.createElement(
213
+ Box,
214
+ { flexDirection: 'column', padding: 1 },
215
+ React.createElement(Text, { color: 'cyan' }, 'Initializing...'),
216
+ );
217
+ }
218
+
219
+ // Render: Scanning
220
+ if (status === 'scanning') {
221
+ return React.createElement(
222
+ Box,
223
+ { flexDirection: 'column', padding: 1 },
224
+ React.createElement(Text, { color: 'cyan' }, `Analyzing ${fileCount} file(s)...`),
225
+ );
226
+ }
227
+
228
+ // Render: No files to scan
229
+ if (status === 'no_files') {
230
+ const noFilesMessages = {
231
+ 'staged-only': 'Stage some changes first with "git add".',
232
+ 'branch-diff': 'No changes found compared to the base branch.',
233
+ 'uncommitted': 'No uncommitted changes found.',
234
+ 'last-commit': 'No files found in the last commit.',
235
+ };
236
+
237
+ return React.createElement(
238
+ Box,
239
+ { flexDirection: 'column', padding: 1 },
240
+ React.createElement(Text, { color: 'yellow' }, 'No files to scan.'),
241
+ React.createElement(Text, { color: 'gray' }, noFilesMessages[scanType] || 'No files found.'),
242
+ );
243
+ }
244
+
245
+ // Render: Error
246
+ if (status === 'error') {
247
+ return React.createElement(
248
+ Box,
249
+ { flexDirection: 'column', padding: 1 },
250
+ React.createElement(Text, { color: 'red' }, '✗ Error: ', error),
251
+ );
252
+ }
253
+
254
+ // Render: Done with issues found
255
+ if (status === 'done' && issues.length > 0) {
256
+ const allIssues = issues.flatMap(file =>
257
+ file.issues.map(i => ({ ...i, file_path: file.file })),
258
+ );
259
+
260
+ const blockingIssues = allIssues.filter(i => shouldFailOn(i.type));
261
+ const nonBlockingIssues = allIssues.filter(i => !shouldFailOn(i.type));
262
+
263
+ const hasBlockingIssues = blockingIssues.length > 0;
264
+
265
+ // Group by file for display
266
+ const groupByFile = (issuesList) => {
267
+ const grouped = Object.create(null);
268
+ issuesList.forEach(i => {
269
+ if (!grouped[i.file_path]) grouped[i.file_path] = [];
270
+ grouped[i.file_path].push(i);
271
+ });
272
+ return grouped;
273
+ };
274
+
275
+ const blockingByFile = groupByFile(blockingIssues);
276
+ const nonBlockingByFile = groupByFile(nonBlockingIssues);
277
+
278
+ // Helper to get color based on issue type
279
+ const getIssueColor = (type) => {
280
+ const t = type?.toUpperCase();
281
+ if (t === 'BLOCKER') return 'red';
282
+ if (t === 'CRITICAL') return 'red';
283
+ if (t === 'MAJOR') return 'red';
284
+ if (t === 'MINOR') return 'yellow';
285
+ return 'gray';
286
+ };
287
+
288
+ const elements = [
289
+ // Header
290
+ React.createElement(
291
+ Text,
292
+ { key: 'header', color: hasBlockingIssues ? 'red' : 'yellow', bold: true },
293
+ hasBlockingIssues
294
+ ? `✗ ${blockingIssues.length} issue(s) found!`
295
+ : `⚠ ${nonBlockingIssues.length} issue(s) found (not blocking)`,
296
+ ),
297
+ React.createElement(Text, { key: 'spacer1' }, ''),
298
+ ];
299
+
300
+ // Blocking issues
301
+ if (blockingIssues.length > 0) {
302
+ Object.entries(blockingByFile).forEach(([filePath, fileIssues]) => {
303
+ elements.push(
304
+ React.createElement(Text, { key: `file-${filePath}`, color: 'yellow', bold: true }, filePath),
305
+ );
306
+ fileIssues.forEach((issue, idx) => {
307
+ const color = getIssueColor(issue.type);
308
+ elements.push(
309
+ React.createElement(
310
+ Text,
311
+ { key: `issue-${filePath}-${idx}`, color },
312
+ ` Line ${issue.line_number}: ${issue.issue_text} (${issue.type})`,
313
+ ),
314
+ );
315
+ // Show fix suggestion if available
316
+ if (issue.fixAvailable && issue.fix?.message) {
317
+ elements.push(
318
+ React.createElement(
319
+ Text,
320
+ { key: `fix-${filePath}-${idx}`, color: 'green', dimColor: true },
321
+ ` 💡 ${issue.fix.message}`,
322
+ ),
323
+ );
324
+ }
325
+ });
326
+ });
327
+ }
328
+
329
+ // Non-blocking issues
330
+ if (nonBlockingIssues.length > 0) {
331
+ if (blockingIssues.length > 0) {
332
+ elements.push(React.createElement(Text, { key: 'spacer2' }, ''));
333
+ }
334
+ elements.push(
335
+ React.createElement(Text, { key: 'nb-header', color: 'gray' }, 'Additional issues (not blocking):')
336
+ );
337
+ Object.entries(nonBlockingByFile).forEach(([filePath, fileIssues]) => {
338
+ elements.push(
339
+ React.createElement(Text, { key: `nb-file-${filePath}`, color: 'gray' }, ` ${filePath}`)
340
+ );
341
+ fileIssues.forEach((issue, idx) => {
342
+ elements.push(
343
+ React.createElement(
344
+ Text,
345
+ { key: `nb-issue-${filePath}-${idx}`, color: 'gray', dimColor: true },
346
+ ` Line ${issue.line_number}: ${issue.issue_text} (${issue.type})`
347
+ )
348
+ );
349
+ });
350
+ });
351
+ }
352
+
353
+ elements.push(React.createElement(Text, { key: 'spacer3' }, ''));
354
+
355
+ // Show auto-fix summary
356
+ if (fixedFiles.length > 0) {
357
+ const totalFixed = fixedFiles.reduce((sum, f) => sum + f.fixedCount, 0);
358
+ elements.push(
359
+ React.createElement(Text, { key: 'autofix', color: 'green' }, `✓ Auto-fixed ${totalFixed} issue(s) in ${fixedFiles.length} file(s)`)
360
+ );
361
+ elements.push(React.createElement(Text, { key: 'spacer4' }, ''));
362
+ }
363
+
364
+ if (hasBlockingIssues) {
365
+ // TODO: change message based on whether auto-fix fixed some issues or not
366
+ const message = (fixedFiles.length > 0)
367
+ ? `✓ Fixed issues automatically. Please review changes before committing.`
368
+ : `Fix issues before committing.`;
369
+ elements.push(
370
+ React.createElement(Text, { key: 'footer', color: 'red' }, message)
371
+ );
372
+ } else {
373
+ elements.push(
374
+ React.createElement(Text, { key: 'footer', color: 'green' }, '✓ Commit allowed (no blocking issues)')
375
+ );
376
+ }
377
+
378
+ return React.createElement(
379
+ Box,
380
+ { flexDirection: 'column', padding: 1 },
381
+ ...elements
382
+ );
383
+ }
384
+
385
+ // Render: Done with no issues
386
+ const elements = [];
387
+
388
+ if (fixedFiles.length > 0) {
389
+ const totalFixed = fixedFiles.reduce((sum, f) => sum + f.fixedCount, 0);
390
+ elements.push(
391
+ React.createElement(Text, { key: 'autofix', color: 'green' }, `✓ Auto-fixed ${totalFixed} issue(s) in ${fixedFiles.length} file(s)`)
392
+ );
393
+ elements.push(React.createElement(Text, { key: 'spacer' }, ''));
394
+ }
395
+
396
+ elements.push(
397
+ React.createElement(Text, { key: 'no-issues', color: 'green' }, '✓ No issues found')
398
+ );
399
+
400
+ return React.createElement(
401
+ Box,
402
+ { flexDirection: 'column', padding: 1 },
403
+ ...elements
404
+ );
405
+ }
@@ -0,0 +1,143 @@
1
+ import React, { useState, useEffect } from 'react';
2
+ import { Text, Box, useApp } from 'ink';
3
+
4
+ const logo = `
5
+ ___ _ _ _
6
+ / __\\___ __| | ___ / \\ _ __ | |_
7
+ / / / _ \\ / _\` |/ _ \\/ /\\ | '_ \\| __|
8
+ / /__| (_) | (_| | __/\\_/ \\ | | | | |_
9
+ \\____/\\___/ \\__,_|\\___\\___/\\_/_| |_|\\__|
10
+ `;
11
+
12
+ const tagline = "Your AI-powered code review companion";
13
+
14
+ const commands = [
15
+ { cmd: 'codeant secrets', desc: 'Scan for secrets in your code' },
16
+ { cmd: 'codeant login', desc: 'Login to CodeAnt' },
17
+ { cmd: 'codeant --help', desc: 'Show all commands' },
18
+ ];
19
+
20
+ export default function Welcome() {
21
+ const { exit } = useApp();
22
+ const [displayedLogo, setDisplayedLogo] = useState('');
23
+ const [displayedTagline, setDisplayedTagline] = useState('');
24
+ const [showCursor, setShowCursor] = useState(true);
25
+ const [phase, setPhase] = useState('logo');
26
+ const [showCommands, setShowCommands] = useState(false);
27
+
28
+ // Typing effect for logo (fast)
29
+ useEffect(() => {
30
+ if (phase !== 'logo') return;
31
+
32
+ let i = 0;
33
+ const chars = logo.split('');
34
+ const interval = setInterval(() => {
35
+ if (i < chars.length) {
36
+ setDisplayedLogo(prev => prev + chars[i]);
37
+ i++;
38
+ } else {
39
+ clearInterval(interval);
40
+ setPhase('tagline');
41
+ }
42
+ }, 2);
43
+
44
+ return () => clearInterval(interval);
45
+ }, [phase]);
46
+
47
+ // Typing effect for tagline (slower, more dramatic)
48
+ useEffect(() => {
49
+ if (phase !== 'tagline') return;
50
+
51
+ let i = 0;
52
+ const chars = tagline.split('');
53
+ const interval = setInterval(() => {
54
+ if (i < chars.length) {
55
+ setDisplayedTagline(prev => prev + chars[i]);
56
+ i++;
57
+ } else {
58
+ clearInterval(interval);
59
+ setPhase('commands');
60
+ setShowCommands(true);
61
+ }
62
+ }, 25);
63
+
64
+ return () => clearInterval(interval);
65
+ }, [phase]);
66
+
67
+ // Blinking cursor
68
+ useEffect(() => {
69
+ if (phase === 'commands') {
70
+ setShowCursor(false);
71
+ return;
72
+ }
73
+ const interval = setInterval(() => {
74
+ setShowCursor(prev => !prev);
75
+ }, 400);
76
+ return () => clearInterval(interval);
77
+ }, [phase]);
78
+
79
+ // Exit after animation
80
+ useEffect(() => {
81
+ if (showCommands) {
82
+ const timeout = setTimeout(() => exit(), 100);
83
+ return () => clearTimeout(timeout);
84
+ }
85
+ }, [showCommands, exit]);
86
+
87
+ const cursor = showCursor ? '|' : ' ';
88
+
89
+ const elements = [
90
+ React.createElement(Text, { key: 'logo', color: 'cyan', bold: true }, displayedLogo)
91
+ ];
92
+
93
+ if (phase !== 'logo') {
94
+ elements.push(
95
+ React.createElement(
96
+ Box,
97
+ { key: 'tagline-box', marginTop: 0 },
98
+ React.createElement(
99
+ Text,
100
+ { color: 'magenta', italic: true },
101
+ displayedTagline,
102
+ React.createElement(Text, { color: 'gray' }, cursor)
103
+ )
104
+ )
105
+ );
106
+ }
107
+
108
+ if (showCommands) {
109
+ elements.push(
110
+ React.createElement(Text, { key: 'divider', color: 'gray' }, '─────────────────────────────────────────')
111
+ );
112
+
113
+ elements.push(
114
+ React.createElement(
115
+ Box,
116
+ { key: 'commands-box', marginTop: 1, flexDirection: 'column' },
117
+ React.createElement(Text, { color: 'yellow', bold: true }, 'Quick Start:'),
118
+ ...commands.map((item, idx) =>
119
+ React.createElement(
120
+ Box,
121
+ { key: `cmd-${idx}`, marginLeft: 2 },
122
+ React.createElement(Text, { color: 'green' }, `$ ${item.cmd}`),
123
+ React.createElement(Text, { color: 'gray' }, ` ${item.desc}`)
124
+ )
125
+ )
126
+ )
127
+ );
128
+
129
+ elements.push(
130
+ React.createElement(
131
+ Box,
132
+ { key: 'version-box', marginTop: 1 },
133
+ React.createElement(Text, { color: 'gray' }, 'v0.1.0')
134
+ )
135
+ );
136
+ }
137
+
138
+ return React.createElement(
139
+ Box,
140
+ { flexDirection: 'column', padding: 1 },
141
+ ...elements
142
+ );
143
+ }
package/src/index.js CHANGED
@@ -8,11 +8,18 @@ import SetBaseUrl from './commands/setBaseUrl.js';
8
8
  import GetBaseUrl from './commands/getBaseUrl.js';
9
9
  import Login from './commands/login.js';
10
10
  import Logout from './commands/logout.js';
11
+ import StaticAnalysis from './commands/staticAnalysis.js';
12
+ import SecurityAnalysis from './commands/securityAnalysis.js';
13
+ import Welcome from './components/Welcome.js';
11
14
 
12
- program
13
- .name('codeant')
14
- .description('Code review CLI tool')
15
- .version('0.1.0');
15
+ // Show welcome animation if no arguments provided
16
+ if (process.argv.length === 2) {
17
+ render(React.createElement(Welcome));
18
+ } else {
19
+ program
20
+ .name('codeant')
21
+ .description('Code review CLI tool')
22
+ .version('0.1.3');
16
23
 
17
24
  program
18
25
  .command('secrets')
@@ -31,11 +38,11 @@ program
31
38
  else if (options.lastCommit) scanType = 'last-commit';
32
39
 
33
40
  const include = options.include
34
- ? (Array.isArray(options.include) ? options.include : options.include.split(',')).map(s => s.trim()).filter(Boolean)
41
+ ? (Array.isArray(options.include) ? options.include : String(options.include).split(',')).map(s => s.trim()).filter(Boolean)
35
42
  : [];
36
43
 
37
44
  const exclude = options.exclude
38
- ? (Array.isArray(options.exclude) ? options.exclude : options.exclude.split(',')).map(s => s.trim()).filter(Boolean)
45
+ ? (Array.isArray(options.exclude) ? options.exclude : String(options.exclude).split(',')).map(s => s.trim()).filter(Boolean)
39
46
  : [];
40
47
 
41
48
  const failOn = options.failOn?.toUpperCase() || 'HIGH';
@@ -44,31 +51,91 @@ program
44
51
  });
45
52
 
46
53
  program
47
- .command('set-base-url <url>')
48
- .description('Set the API base URL')
49
- .action((url) => {
50
- render(React.createElement(SetBaseUrl, { url }));
51
- });
54
+ .command('static-analysis')
55
+ .description('Run static analysis on your code')
56
+ .option('--staged', 'Scan only staged files (default)')
57
+ .option('--all', 'Scan all changed files')
58
+ .option('--uncommitted', 'Scan uncommitted changes')
59
+ .option('--last-commit', 'Scan last commit')
60
+ .option('--fail-on <level>', 'Fail on issues at or above this level: BLOCKER, CRITICAL, MAJOR, MINOR, INFO (default: CRITICAL)', 'CRITICAL')
61
+ .option('--auto-fix', 'Automatically apply fixes when available')
62
+ .option('--include <paths>', 'Comma-separated list of file paths regex to include')
63
+ .option('--exclude <paths>', 'Comma-separated list of file paths regex to exclude')
64
+ .action((options) => {
65
+ let scanType = 'staged-only';
66
+ if (options.all) scanType = 'branch-diff';
67
+ else if (options.uncommitted) scanType = 'uncommitted';
68
+ else if (options.lastCommit) scanType = 'last-commit';
52
69
 
53
- program
54
- .command('get-base-url')
55
- .description('Show the current API base URL')
56
- .action(() => {
57
- render(React.createElement(GetBaseUrl));
70
+ const include = options.include
71
+ ? (Array.isArray(options.include) ? options.include : String(options.include).split(',')).map(s => s.trim()).filter(Boolean)
72
+ : [];
73
+
74
+ const exclude = options.exclude
75
+ ? (Array.isArray(options.exclude) ? options.exclude : String(options.exclude).split(',')).map(s => s.trim()).filter(Boolean)
76
+ : [];
77
+
78
+ const failOn = options.failOn?.toUpperCase() || 'CRITICAL';
79
+ const autoFix = options.autoFix || false;
80
+ render(React.createElement(StaticAnalysis, {scanType, failOn, include, exclude, autoFix}));
58
81
  });
59
82
 
60
83
  program
61
- .command('login')
62
- .description('Login to CodeAnt')
63
- .action(() => {
64
- render(React.createElement(Login));
84
+ .command('security-analysis')
85
+ .description('Run security analysis on your code')
86
+ .option('--staged', 'Scan only staged files (default)')
87
+ .option('--all', 'Scan all changed files')
88
+ .option('--uncommitted', 'Scan uncommitted changes')
89
+ .option('--last-commit', 'Scan last commit')
90
+ .option('--fail-on <level>', 'Fail on issues at or above this level: BLOCKER, CRITICAL, MAJOR, MINOR, INFO (default: CRITICAL)', 'CRITICAL')
91
+ .option('--include <paths>', 'Comma-separated list of file paths regex to include')
92
+ .option('--exclude <paths>', 'Comma-separated list of file paths regex to exclude')
93
+ .action((options) => {
94
+ let scanType = 'staged-only';
95
+ if (options.all) scanType = 'branch-diff';
96
+ else if (options.uncommitted) scanType = 'uncommitted';
97
+ else if (options.lastCommit) scanType = 'last-commit';
98
+
99
+ const include = options.include
100
+ ? (Array.isArray(options.include) ? options.include : String(options.include).split(',')).map(s => s.trim()).filter(Boolean)
101
+ : [];
102
+
103
+ const exclude = options.exclude
104
+ ? (Array.isArray(options.exclude) ? options.exclude : String(options.exclude).split(',')).map(s => s.trim()).filter(Boolean)
105
+ : [];
106
+
107
+ const failOn = options.failOn?.toUpperCase() || 'HIGH';
108
+
109
+ render(React.createElement(SecurityAnalysis, { scanType, failOn, include, exclude }));
65
110
  });
66
111
 
67
112
  program
68
- .command('logout')
69
- .description('Logout from CodeAnt')
70
- .action(() => {
71
- render(React.createElement(Logout));
113
+ .command('set-base-url <url>')
114
+ .description('Set the API base URL')
115
+ .action((url) => {
116
+ render(React.createElement(SetBaseUrl, { url }));
72
117
  });
73
118
 
74
- program.parse();
119
+ program
120
+ .command('get-base-url')
121
+ .description('Show the current API base URL')
122
+ .action(() => {
123
+ render(React.createElement(GetBaseUrl));
124
+ });
125
+
126
+ program
127
+ .command('login')
128
+ .description('Login to CodeAnt')
129
+ .action(() => {
130
+ render(React.createElement(Login));
131
+ });
132
+
133
+ program
134
+ .command('logout')
135
+ .description('Logout from CodeAnt')
136
+ .action(() => {
137
+ render(React.createElement(Logout));
138
+ });
139
+
140
+ program.parse();
141
+ }