codeant-cli 0.1.5 → 0.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/239.index.js +1 -0
- package/dist/701.index.js +1 -0
- package/dist/935.index.js +1 -0
- package/dist/index.js +12 -0
- package/dist/package.json +3 -0
- package/package.json +12 -4
- package/src/commands/getBaseUrl.js +0 -25
- package/src/commands/login.js +0 -124
- package/src/commands/logout.js +0 -30
- package/src/commands/review.js +0 -239
- package/src/commands/secrets.js +0 -268
- package/src/commands/securityAnalysis.js +0 -284
- package/src/commands/setBaseUrl.js +0 -37
- package/src/commands/staticAnalysis.js +0 -405
- package/src/components/ReviewUI.js +0 -127
- package/src/components/Welcome.js +0 -143
- package/src/index.js +0 -202
- package/src/tools/bashTool.js +0 -17
- package/src/tools/executeTool.js +0 -35
- package/src/tools/globTool.js +0 -9
- package/src/tools/grepTool.js +0 -18
- package/src/tools/lsTool.js +0 -7
- package/src/tools/readTool.js +0 -12
- package/src/utils/commonApiHelper.js +0 -101
- package/src/utils/config.js +0 -42
- package/src/utils/fetchApi.js +0 -42
- package/src/utils/gitDiffHelper.js +0 -774
- package/src/utils/reviewApiHelper.js +0 -139
- package/src/utils/secretsApiHelper.js +0 -134
- package/src/utils/securityAnalysisApiHelper.js +0 -119
- package/src/utils/staticAnalysisApiHelper.js +0 -119
|
@@ -1,405 +0,0 @@
|
|
|
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, lastNCommits = 1 }) {
|
|
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, { lastNCommits });
|
|
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, lastNCommits]);
|
|
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
|
-
}
|
|
@@ -1,127 +0,0 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
import { Box, Text } from 'ink';
|
|
3
|
-
|
|
4
|
-
export function mapLabelToSeverity(label) {
|
|
5
|
-
const mapping = {
|
|
6
|
-
'security': 'CRITICAL',
|
|
7
|
-
'performance': 'MAJOR',
|
|
8
|
-
'maintainability': 'MINOR',
|
|
9
|
-
'code quality': 'MINOR',
|
|
10
|
-
'best practice': 'MINOR',
|
|
11
|
-
'bug': 'MAJOR',
|
|
12
|
-
};
|
|
13
|
-
const normalizedLabel = label.toLowerCase().replace(/[^\w\s]/g, '');
|
|
14
|
-
return mapping[normalizedLabel] || 'MINOR';
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export function renderInitializing() {
|
|
18
|
-
return React.createElement(Box, { flexDirection: 'column', padding: 1 },
|
|
19
|
-
React.createElement(Text, { color: 'cyan' }, 'Initializing review...'));
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export function renderFetchingDiff() {
|
|
23
|
-
return React.createElement(Box, { flexDirection: 'column', padding: 1 },
|
|
24
|
-
React.createElement(Text, { color: 'cyan' }, 'Fetching changes...'));
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export function renderReviewing(currentMessage) {
|
|
28
|
-
return React.createElement(Box, { flexDirection: 'column', padding: 1 },
|
|
29
|
-
React.createElement(Text, { color: 'cyan' }, currentMessage || 'Reviewing code...'));
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export function renderNoFiles(scanType, lastNCommits) {
|
|
33
|
-
const noFilesMessages = {
|
|
34
|
-
'staged-only': 'Stage some changes first with "git add".',
|
|
35
|
-
'branch-diff': 'No changes found compared to the base branch.',
|
|
36
|
-
'uncommitted': 'No uncommitted changes found.',
|
|
37
|
-
'last-commit': 'No files found in the last commit.',
|
|
38
|
-
'last-n-commits': `No changes found in the last ${lastNCommits} commit(s).`,
|
|
39
|
-
};
|
|
40
|
-
return React.createElement(Box, { flexDirection: 'column', padding: 1 },
|
|
41
|
-
React.createElement(Text, { color: 'yellow' }, 'No files to review.'),
|
|
42
|
-
React.createElement(Text, { color: 'gray' }, noFilesMessages[scanType] || 'No files found.'));
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export function renderError(error) {
|
|
46
|
-
return React.createElement(Box, { flexDirection: 'column', padding: 1 },
|
|
47
|
-
React.createElement(Text, { color: 'red' }, `✗ Error: ${error}`));
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
export function renderNotLoggedIn() {
|
|
51
|
-
return React.createElement(
|
|
52
|
-
Box, { flexDirection: 'column', padding: 1 },
|
|
53
|
-
React.createElement(Text, { color: 'red' }, '✗ Not logged in.'),
|
|
54
|
-
React.createElement(Text, { color: 'gray' }, 'Run "codeant login" to authenticate.'),
|
|
55
|
-
);
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
export function renderDone(issues, summary, failOn) {
|
|
59
|
-
const severityOrder = ['INFO', 'MINOR', 'MAJOR', 'CRITICAL', 'BLOCKER'];
|
|
60
|
-
const failOnIndex = severityOrder.indexOf(failOn);
|
|
61
|
-
if (failOnIndex === -1) {
|
|
62
|
-
return React.createElement(Box, { flexDirection: 'column', padding: 1 },
|
|
63
|
-
React.createElement(Text, { color: 'red', bold: true }, `✗ Invalid failOn value: ${failOn}`));
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
const blockingIssues = issues.filter(issue =>
|
|
67
|
-
severityOrder.indexOf(mapLabelToSeverity(issue.label)) >= failOnIndex
|
|
68
|
-
);
|
|
69
|
-
const nonBlockingIssues = issues.filter(issue =>
|
|
70
|
-
severityOrder.indexOf(mapLabelToSeverity(issue.label)) < failOnIndex
|
|
71
|
-
);
|
|
72
|
-
const hasBlocking = blockingIssues.length > 0;
|
|
73
|
-
|
|
74
|
-
const elements = [];
|
|
75
|
-
|
|
76
|
-
if (issues.length > 0) {
|
|
77
|
-
elements.push(React.createElement(Text, { key: 'header', color: hasBlocking ? 'red' : 'yellow', bold: true },
|
|
78
|
-
hasBlocking
|
|
79
|
-
? `✗ ${blockingIssues.length} blocking issue(s) found!`
|
|
80
|
-
: `⚠ ${issues.length} issue(s) found (non-blocking)`
|
|
81
|
-
));
|
|
82
|
-
} else {
|
|
83
|
-
elements.push(React.createElement(Text, { key: 'header', color: 'green', bold: true }, '✓ No issues found'));
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
elements.push(React.createElement(Text, { key: 'sp1' }, ''));
|
|
87
|
-
|
|
88
|
-
blockingIssues.forEach((issue, idx) => {
|
|
89
|
-
const sev = mapLabelToSeverity(issue.label);
|
|
90
|
-
elements.push(React.createElement(Text, { key: `bi-${idx}`, color: 'red', bold: true },
|
|
91
|
-
` [${sev}] ${issue.relevant_file || 'Unknown'}:${issue.start_line || '?'} - ${issue.label}`));
|
|
92
|
-
if (issue.issue_content) {
|
|
93
|
-
elements.push(React.createElement(Text, { key: `bi-msg-${idx}`, color: 'white' },
|
|
94
|
-
` ${issue.issue_content}`));
|
|
95
|
-
}
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
if (nonBlockingIssues.length > 0) {
|
|
99
|
-
if (blockingIssues.length > 0) elements.push(React.createElement(Text, { key: 'sp2' }, ''));
|
|
100
|
-
elements.push(React.createElement(Text, { key: 'nb-hdr', color: 'gray' }, 'Non-blocking issues:'));
|
|
101
|
-
nonBlockingIssues.forEach((issue, idx) => {
|
|
102
|
-
const sev = mapLabelToSeverity(issue.label);
|
|
103
|
-
elements.push(React.createElement(Text, { key: `nb-${idx}`, color: 'yellow' },
|
|
104
|
-
` [${sev}] ${issue.relevant_file || 'Unknown'}:${issue.start_line || '?'} - ${issue.label}`));
|
|
105
|
-
if (issue.issue_content) {
|
|
106
|
-
elements.push(React.createElement(Text, { key: `nb-msg-${idx}`, color: 'gray' },
|
|
107
|
-
` ${issue.issue_content}`));
|
|
108
|
-
}
|
|
109
|
-
});
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
if (summary?.message) {
|
|
113
|
-
elements.push(React.createElement(Text, { key: 'sp3' }, ''));
|
|
114
|
-
elements.push(React.createElement(Text, { key: 'sum-hdr', color: 'cyan', bold: true }, 'Summary:'));
|
|
115
|
-
elements.push(React.createElement(Text, { key: 'sum-txt', color: 'white' }, summary.message));
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
elements.push(React.createElement(Text, { key: 'sp4' }, ''));
|
|
119
|
-
|
|
120
|
-
if (hasBlocking) {
|
|
121
|
-
elements.push(React.createElement(Text, { key: 'footer', color: 'red' }, 'Fix blocking issues before proceeding.'));
|
|
122
|
-
} else if (issues.length > 0) {
|
|
123
|
-
elements.push(React.createElement(Text, { key: 'footer', color: 'green' }, '✓ No blocking issues found'));
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
return React.createElement(Box, { flexDirection: 'column', padding: 1 }, ...elements);
|
|
127
|
-
}
|
|
@@ -1,143 +0,0 @@
|
|
|
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
|
-
}
|