codeant-cli 0.3.3 → 0.3.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +8 -10
- package/src/commands/getBaseUrl.js +21 -0
- package/src/commands/login.js +120 -0
- package/src/commands/logout.js +30 -0
- package/src/commands/review.js +294 -0
- package/src/commands/secrets.js +107 -0
- package/src/commands/setBaseUrl.js +37 -0
- package/src/components/ReviewUI.js +410 -0
- package/src/components/SecretsUI.js +366 -0
- package/src/components/Welcome.js +144 -0
- package/src/index.js +390 -0
- package/src/reviewHeadless.js +198 -0
- package/src/rules/secrets.toml +4994 -0
- package/src/scm/azure/client.js +21 -0
- package/src/scm/azure/getCodeReview.js +20 -0
- package/src/scm/azure/getPullRequest.js +30 -0
- package/src/scm/azure/index.js +7 -0
- package/src/scm/azure/listCodeReviews.js +17 -0
- package/src/scm/azure/listPullRequestComments.js +41 -0
- package/src/scm/azure/listPullRequests.js +26 -0
- package/src/scm/azure/resolveConversation.js +22 -0
- package/src/scm/azure/searchComments.js +35 -0
- package/src/scm/bitbucket/client.js +18 -0
- package/src/scm/bitbucket/getCodeReview.js +3 -0
- package/src/scm/bitbucket/getPullRequest.js +26 -0
- package/src/scm/bitbucket/index.js +7 -0
- package/src/scm/bitbucket/listCodeReviews.js +17 -0
- package/src/scm/bitbucket/listPullRequestComments.js +33 -0
- package/src/scm/bitbucket/listPullRequests.js +33 -0
- package/src/scm/bitbucket/resolveConversation.js +21 -0
- package/src/scm/bitbucket/searchComments.js +29 -0
- package/src/scm/github/client.js +17 -0
- package/src/scm/github/getCodeReview.js +26 -0
- package/src/scm/github/getPullRequest.js +27 -0
- package/src/scm/github/index.js +7 -0
- package/src/scm/github/listCodeReviews.js +17 -0
- package/src/scm/github/listPullRequestComments.js +51 -0
- package/src/scm/github/listPullRequests.js +32 -0
- package/src/scm/github/resolveConversation.js +85 -0
- package/src/scm/github/searchComments.js +30 -0
- package/src/scm/gitlab/client.js +16 -0
- package/src/scm/gitlab/getCodeReview.js +18 -0
- package/src/scm/gitlab/getPullRequest.js +24 -0
- package/src/scm/gitlab/index.js +7 -0
- package/src/scm/gitlab/listCodeReviews.js +21 -0
- package/src/scm/gitlab/listPullRequestComments.js +44 -0
- package/src/scm/gitlab/listPullRequests.js +26 -0
- package/src/scm/gitlab/resolveConversation.js +14 -0
- package/src/scm/gitlab/searchComments.js +28 -0
- package/src/scm/index.js +97 -0
- package/src/tools/executeTool.js +31 -0
- package/src/tools/globTool.js +9 -0
- package/src/tools/grepTool.js +18 -0
- package/src/tools/lsTool.js +7 -0
- package/src/tools/readTool.js +12 -0
- package/src/utils/analytics.js +56 -0
- package/src/utils/baseUrl.js +11 -0
- package/src/utils/commonApiHelper.js +143 -0
- package/src/utils/config.js +42 -0
- package/src/utils/fetchApi.js +39 -0
- package/src/utils/gitDiffHelper.js +1203 -0
- package/src/utils/reviewApiHelper.js +291 -0
- package/src/utils/scmAuth.js +136 -0
- package/src/utils/secretsApiHelper.js +252 -0
- package/src/utils/secretsDetector.js +210 -0
- package/dist/178.index.js +0 -1
- package/dist/239.index.js +0 -1
- package/dist/701.index.js +0 -1
- package/dist/880.index.js +0 -1
- package/dist/935.index.js +0 -1
- package/dist/966.index.js +0 -1
- package/dist/index.js +0 -28
- package/dist/package.json +0 -1
- package/dist/review/239.index.js +0 -1
- package/dist/review/index.js +0 -8
- package/dist/review/package.json +0 -3
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
import React, { useState, useEffect } from 'react';
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
|
|
4
|
+
// ── Constants ────────────────────────────────────────────────────────────────
|
|
5
|
+
|
|
6
|
+
const SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
7
|
+
const DIVIDER = '─'.repeat(55);
|
|
8
|
+
const STEPS = ['Init', 'Fetch', 'Scan'];
|
|
9
|
+
|
|
10
|
+
const CONFIDENCE_COLORS = {
|
|
11
|
+
HIGH: 'red',
|
|
12
|
+
MEDIUM: 'yellow',
|
|
13
|
+
LOW: 'cyan',
|
|
14
|
+
FALSE_POSITIVE: 'gray',
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
// ── Helpers ──────────────────────────────────────────────────────────────────
|
|
18
|
+
|
|
19
|
+
function getConfidenceColor(score) {
|
|
20
|
+
return CONFIDENCE_COLORS[score?.toUpperCase()] || 'gray';
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function padEnd(str, len) {
|
|
24
|
+
return str + ' '.repeat(Math.max(0, len - str.length));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function formatElapsed(startTime) {
|
|
28
|
+
return ((Date.now() - startTime) / 1000).toFixed(1) + 's';
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// ── Animated Components ──────────────────────────────────────────────────────
|
|
32
|
+
|
|
33
|
+
function Spinner({ color = 'cyan' }) {
|
|
34
|
+
const [frame, setFrame] = useState(0);
|
|
35
|
+
useEffect(() => {
|
|
36
|
+
const timer = setInterval(() => {
|
|
37
|
+
setFrame(f => (f + 1) % SPINNER_FRAMES.length);
|
|
38
|
+
}, 80);
|
|
39
|
+
return () => clearInterval(timer);
|
|
40
|
+
}, []);
|
|
41
|
+
return React.createElement(Text, { color }, SPINNER_FRAMES[frame]);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function ElapsedTime({ startTime }) {
|
|
45
|
+
const [, setTick] = useState(0);
|
|
46
|
+
useEffect(() => {
|
|
47
|
+
const timer = setInterval(() => setTick(t => t + 1), 100);
|
|
48
|
+
return () => clearInterval(timer);
|
|
49
|
+
}, []);
|
|
50
|
+
return React.createElement(Text, { color: 'gray' }, formatElapsed(startTime));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function StepProgress({ currentStep }) {
|
|
54
|
+
const items = STEPS.map((step, i) => {
|
|
55
|
+
if (i < currentStep) {
|
|
56
|
+
return React.createElement(Text, { key: step, color: 'green' }, `✓ ${step}`);
|
|
57
|
+
} else if (i === currentStep) {
|
|
58
|
+
return React.createElement(
|
|
59
|
+
Box, { key: step },
|
|
60
|
+
React.createElement(Spinner, { color: 'cyan' }),
|
|
61
|
+
React.createElement(Text, { color: 'cyan' }, ` ${step}`),
|
|
62
|
+
);
|
|
63
|
+
} else {
|
|
64
|
+
return React.createElement(Text, { key: step, color: 'gray', dimColor: true }, `· ${step}`);
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
return React.createElement(Box, { gap: 2 }, ...items);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// ── Loading Screen ───────────────────────────────────────────────────────────
|
|
71
|
+
|
|
72
|
+
function SecretsLoading({ message, step, startTime, fileCount, meta }) {
|
|
73
|
+
const els = [
|
|
74
|
+
React.createElement(
|
|
75
|
+
Box, { key: 'status', gap: 1 },
|
|
76
|
+
React.createElement(Spinner, {}),
|
|
77
|
+
React.createElement(Text, { color: 'white', bold: true }, message),
|
|
78
|
+
startTime != null && React.createElement(ElapsedTime, { startTime }),
|
|
79
|
+
),
|
|
80
|
+
React.createElement(
|
|
81
|
+
Box, { key: 'steps', marginTop: 1 },
|
|
82
|
+
React.createElement(StepProgress, { currentStep: step }),
|
|
83
|
+
),
|
|
84
|
+
];
|
|
85
|
+
|
|
86
|
+
// Show file list and coverage info when we have metadata (during scan step)
|
|
87
|
+
if (meta && step >= 2) {
|
|
88
|
+
const scanned = meta.scanned_files || [];
|
|
89
|
+
const total = meta.total_changed || 0;
|
|
90
|
+
const skipped = meta.skipped || [];
|
|
91
|
+
|
|
92
|
+
els.push(React.createElement(Text, { key: 'meta-sp' }, ''));
|
|
93
|
+
els.push(React.createElement(
|
|
94
|
+
Text, { key: 'meta-hdr', color: 'gray', dimColor: true }, DIVIDER,
|
|
95
|
+
));
|
|
96
|
+
|
|
97
|
+
// Coverage line
|
|
98
|
+
if (total > 0) {
|
|
99
|
+
const coverageColor = scanned.length === total ? 'green' : 'yellow';
|
|
100
|
+
els.push(React.createElement(
|
|
101
|
+
Text, { key: 'coverage', color: coverageColor },
|
|
102
|
+
`Scanning ${scanned.length} of ${total} changed file${total !== 1 ? 's' : ''}`,
|
|
103
|
+
));
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// File list
|
|
107
|
+
if (scanned.length > 0) {
|
|
108
|
+
els.push(React.createElement(Text, { key: 'files-sp' }, ''));
|
|
109
|
+
scanned.forEach((file, i) => {
|
|
110
|
+
const isLast = i === scanned.length - 1;
|
|
111
|
+
const branch = isLast ? '└─' : '├─';
|
|
112
|
+
els.push(React.createElement(
|
|
113
|
+
Text, { key: `file-${i}`, color: 'white' },
|
|
114
|
+
` ${branch} ${file}`,
|
|
115
|
+
));
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Skipped summary
|
|
120
|
+
if (skipped.length > 0) {
|
|
121
|
+
els.push(React.createElement(Text, { key: 'skip-sp' }, ''));
|
|
122
|
+
const reasons = {};
|
|
123
|
+
skipped.forEach(s => {
|
|
124
|
+
const r = s.reason.split(' (')[0];
|
|
125
|
+
reasons[r] = (reasons[r] || 0) + 1;
|
|
126
|
+
});
|
|
127
|
+
const parts = Object.entries(reasons).map(([r, n]) => `${n} ${r}`);
|
|
128
|
+
els.push(React.createElement(
|
|
129
|
+
Text, { key: 'skipped', color: 'gray', dimColor: true },
|
|
130
|
+
`Skipped: ${parts.join(' · ')}`,
|
|
131
|
+
));
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return React.createElement(
|
|
136
|
+
Box, { flexDirection: 'column', paddingX: 1, paddingY: 1 }, ...els,
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// ── Public Render Functions ──────────────────────────────────────────────────
|
|
141
|
+
|
|
142
|
+
export function renderInitializing(startTime) {
|
|
143
|
+
return React.createElement(SecretsLoading, {
|
|
144
|
+
message: 'Initializing...',
|
|
145
|
+
step: 0,
|
|
146
|
+
startTime,
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function renderFetchingDiff(startTime) {
|
|
151
|
+
return React.createElement(SecretsLoading, {
|
|
152
|
+
message: 'Fetching changes...',
|
|
153
|
+
step: 1,
|
|
154
|
+
startTime,
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export function renderScanning(startTime, fileCount, meta) {
|
|
159
|
+
return React.createElement(SecretsLoading, {
|
|
160
|
+
message: 'Scanning for secrets...',
|
|
161
|
+
step: 2,
|
|
162
|
+
startTime,
|
|
163
|
+
fileCount,
|
|
164
|
+
meta,
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export function renderNoFiles(scanType, lastNCommits, baseBranch, baseCommit) {
|
|
169
|
+
const hints = {
|
|
170
|
+
'staged-only': 'Stage some changes first with "git add".',
|
|
171
|
+
'branch-diff': 'No changes found compared to the base branch.',
|
|
172
|
+
'uncommitted': 'No uncommitted changes found.',
|
|
173
|
+
'committed': 'No unpushed commits found.',
|
|
174
|
+
'all': 'No changes found between your branch and the base.',
|
|
175
|
+
'last-commit': 'No files found in the last commit.',
|
|
176
|
+
'last-n-commits': `No changes found in the last ${lastNCommits} commit(s).`,
|
|
177
|
+
'base-branch': `No changes found compared to ${baseBranch}.`,
|
|
178
|
+
'base-commit': `No changes found compared to commit ${baseCommit}.`,
|
|
179
|
+
};
|
|
180
|
+
return React.createElement(
|
|
181
|
+
Box, { flexDirection: 'column', paddingX: 1, paddingY: 1 },
|
|
182
|
+
React.createElement(Text, { color: 'cyan', bold: true }, 'CodeAnt Secrets'),
|
|
183
|
+
React.createElement(Text, { color: 'gray', dimColor: true }, DIVIDER),
|
|
184
|
+
React.createElement(Text, {}, ''),
|
|
185
|
+
React.createElement(Text, { color: 'yellow' }, ' No changes to scan.'),
|
|
186
|
+
React.createElement(Text, { color: 'gray' }, ` ${hints[scanType] || 'No files found.'}`),
|
|
187
|
+
React.createElement(Text, {}, ''),
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export function renderError(error) {
|
|
192
|
+
return React.createElement(
|
|
193
|
+
Box, { flexDirection: 'column', paddingX: 1, paddingY: 1 },
|
|
194
|
+
React.createElement(Text, { color: 'cyan', bold: true }, 'CodeAnt Secrets'),
|
|
195
|
+
React.createElement(Text, { color: 'gray', dimColor: true }, DIVIDER),
|
|
196
|
+
React.createElement(Text, {}, ''),
|
|
197
|
+
React.createElement(Text, { color: 'red', bold: true }, ` ✗ ${error}`),
|
|
198
|
+
React.createElement(Text, {}, ''),
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export function renderNotLoggedIn() {
|
|
203
|
+
return React.createElement(
|
|
204
|
+
Box, { flexDirection: 'column', paddingX: 1, paddingY: 1 },
|
|
205
|
+
React.createElement(Text, { color: 'cyan', bold: true }, 'CodeAnt Secrets'),
|
|
206
|
+
React.createElement(Text, { color: 'gray', dimColor: true }, DIVIDER),
|
|
207
|
+
React.createElement(Text, {}, ''),
|
|
208
|
+
React.createElement(Text, { color: 'red', bold: true }, ' ✗ Not logged in'),
|
|
209
|
+
React.createElement(Text, { color: 'gray' }, ' Run "codeant login" to authenticate.'),
|
|
210
|
+
React.createElement(Text, {}, ''),
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export function renderDone(secrets, failOn, shouldFailOn, startTime, fileCount, meta) {
|
|
215
|
+
const allSecrets = secrets.flatMap(file =>
|
|
216
|
+
file.secrets.map(s => ({ ...s, file_path: file.file_path }))
|
|
217
|
+
);
|
|
218
|
+
|
|
219
|
+
const blockingSecrets = allSecrets.filter(s => shouldFailOn(s.confidence_score));
|
|
220
|
+
const hasBlocking = blockingSecrets.length > 0;
|
|
221
|
+
const elapsed = startTime ? formatElapsed(startTime) : null;
|
|
222
|
+
|
|
223
|
+
// Count by confidence
|
|
224
|
+
const counts = {};
|
|
225
|
+
allSecrets.forEach(s => {
|
|
226
|
+
const conf = s.confidence_score?.toUpperCase() || 'UNKNOWN';
|
|
227
|
+
counts[conf] = (counts[conf] || 0) + 1;
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
// Group by file
|
|
231
|
+
const grouped = {};
|
|
232
|
+
allSecrets.forEach(s => {
|
|
233
|
+
const file = s.file_path || 'Unknown';
|
|
234
|
+
(grouped[file] ??= []).push(s);
|
|
235
|
+
});
|
|
236
|
+
const sortedFiles = Object.keys(grouped).sort();
|
|
237
|
+
sortedFiles.forEach(f =>
|
|
238
|
+
grouped[f].sort((a, b) => (a.line_number || 0) - (b.line_number || 0))
|
|
239
|
+
);
|
|
240
|
+
|
|
241
|
+
const els = [];
|
|
242
|
+
|
|
243
|
+
// ── Header ──
|
|
244
|
+
els.push(React.createElement(
|
|
245
|
+
Box, { key: 'hdr', gap: 1 },
|
|
246
|
+
React.createElement(Text, { color: 'cyan', bold: true }, 'CodeAnt Secrets'),
|
|
247
|
+
elapsed && React.createElement(Text, { color: 'gray' }, elapsed),
|
|
248
|
+
));
|
|
249
|
+
els.push(React.createElement(
|
|
250
|
+
Text, { key: 'div1', color: 'gray', dimColor: true }, DIVIDER,
|
|
251
|
+
));
|
|
252
|
+
|
|
253
|
+
if (allSecrets.length === 0) {
|
|
254
|
+
// ── Clean pass ──
|
|
255
|
+
els.push(React.createElement(Text, { key: 'sp' }, ''));
|
|
256
|
+
els.push(React.createElement(
|
|
257
|
+
Text, { key: 'ok', color: 'green', bold: true }, ' ✓ No secrets found',
|
|
258
|
+
));
|
|
259
|
+
els.push(React.createElement(Text, { key: 'sp2' }, ''));
|
|
260
|
+
} else {
|
|
261
|
+
// ── Secrets grouped by file ──
|
|
262
|
+
els.push(React.createElement(Text, { key: 'sp1' }, ''));
|
|
263
|
+
|
|
264
|
+
sortedFiles.forEach((file, fi) => {
|
|
265
|
+
const fileSecrets = grouped[file];
|
|
266
|
+
|
|
267
|
+
els.push(React.createElement(
|
|
268
|
+
Text, { key: `f-${fi}`, color: 'white', bold: true }, ` ${file}`,
|
|
269
|
+
));
|
|
270
|
+
|
|
271
|
+
fileSecrets.forEach((secret, si) => {
|
|
272
|
+
const conf = secret.confidence_score?.toUpperCase() || 'UNKNOWN';
|
|
273
|
+
const confColor = getConfidenceColor(conf);
|
|
274
|
+
const isBlocking = shouldFailOn(secret.confidence_score);
|
|
275
|
+
const isLast = si === fileSecrets.length - 1;
|
|
276
|
+
const branch = isLast ? '└─' : '├─';
|
|
277
|
+
const lineStr = secret.line_number ? `L${secret.line_number}` : '';
|
|
278
|
+
|
|
279
|
+
els.push(React.createElement(
|
|
280
|
+
Box, { key: `s-${fi}-${si}` },
|
|
281
|
+
React.createElement(Text, { color: 'gray' }, ` ${branch} `),
|
|
282
|
+
React.createElement(Text, {
|
|
283
|
+
color: confColor,
|
|
284
|
+
bold: isBlocking,
|
|
285
|
+
}, padEnd(conf, 16)),
|
|
286
|
+
React.createElement(Text, { color: 'gray' }, padEnd(lineStr, 6)),
|
|
287
|
+
React.createElement(Text, { color: isBlocking ? 'white' : 'gray', dimColor: !isBlocking }, secret.type || ''),
|
|
288
|
+
));
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
els.push(React.createElement(Text, { key: `g-${fi}` }, ''));
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// ── Footer ──
|
|
296
|
+
els.push(React.createElement(
|
|
297
|
+
Text, { key: 'div2', color: 'gray', dimColor: true }, DIVIDER,
|
|
298
|
+
));
|
|
299
|
+
|
|
300
|
+
// Coverage line
|
|
301
|
+
const totalChanged = meta?.total_changed || 0;
|
|
302
|
+
const scannedCount = meta?.scanned_files?.length || fileCount;
|
|
303
|
+
if (totalChanged > 0) {
|
|
304
|
+
const coverageColor = scannedCount === totalChanged ? 'green' : 'yellow';
|
|
305
|
+
els.push(React.createElement(
|
|
306
|
+
Text, { key: 'coverage', color: coverageColor },
|
|
307
|
+
`Scanned ${scannedCount} of ${totalChanged} changed file${totalChanged !== 1 ? 's' : ''}`,
|
|
308
|
+
));
|
|
309
|
+
} else if (fileCount) {
|
|
310
|
+
els.push(React.createElement(
|
|
311
|
+
Text, { key: 'coverage', color: 'gray' },
|
|
312
|
+
`${fileCount} file${fileCount !== 1 ? 's' : ''} scanned`,
|
|
313
|
+
));
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// Skipped files summary
|
|
317
|
+
const skipped = meta?.skipped || [];
|
|
318
|
+
if (skipped.length > 0) {
|
|
319
|
+
const reasons = {};
|
|
320
|
+
skipped.forEach(s => {
|
|
321
|
+
const r = s.reason.split(' (')[0];
|
|
322
|
+
reasons[r] = (reasons[r] || 0) + 1;
|
|
323
|
+
});
|
|
324
|
+
const parts = Object.entries(reasons).map(([r, n]) => `${n} ${r}`);
|
|
325
|
+
els.push(React.createElement(
|
|
326
|
+
Text, { key: 'skipped', color: 'gray', dimColor: true },
|
|
327
|
+
`Skipped: ${parts.join(' · ')}`,
|
|
328
|
+
));
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// Secret stats line
|
|
332
|
+
if (allSecrets.length > 0) {
|
|
333
|
+
const stats = [`${allSecrets.length} secret${allSecrets.length !== 1 ? 's' : ''} found`];
|
|
334
|
+
for (const conf of ['HIGH', 'MEDIUM', 'LOW', 'FALSE_POSITIVE']) {
|
|
335
|
+
if (counts[conf]) {
|
|
336
|
+
const label = conf === 'FALSE_POSITIVE' ? 'false positive' : conf.toLowerCase();
|
|
337
|
+
stats.push(`${counts[conf]} ${label}`);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
els.push(React.createElement(
|
|
341
|
+
Text, { key: 'stats', color: 'gray' }, stats.join(' · '),
|
|
342
|
+
));
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// Status line
|
|
346
|
+
if (hasBlocking) {
|
|
347
|
+
els.push(React.createElement(
|
|
348
|
+
Text, { key: 'status', color: 'red', bold: true },
|
|
349
|
+
`✗ ${blockingSecrets.length} blocking secret${blockingSecrets.length !== 1 ? 's' : ''} — remove before committing`,
|
|
350
|
+
));
|
|
351
|
+
} else if (allSecrets.length > 0) {
|
|
352
|
+
els.push(React.createElement(
|
|
353
|
+
Text, { key: 'status', color: 'green', bold: true },
|
|
354
|
+
'✓ No blocking secrets found',
|
|
355
|
+
));
|
|
356
|
+
} else {
|
|
357
|
+
els.push(React.createElement(
|
|
358
|
+
Text, { key: 'status', color: 'green', bold: true },
|
|
359
|
+
'✓ All clear',
|
|
360
|
+
));
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
return React.createElement(
|
|
364
|
+
Box, { flexDirection: 'column', paddingX: 1, paddingY: 1 }, ...els,
|
|
365
|
+
);
|
|
366
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
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 review', desc: 'Run AI-powered code review' },
|
|
16
|
+
{ cmd: 'codeant secrets', desc: 'Scan for secrets in your code' },
|
|
17
|
+
{ cmd: 'codeant login', desc: 'Login to CodeAnt' },
|
|
18
|
+
{ cmd: 'codeant --help', desc: 'Show all commands' },
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
export default function Welcome({ version }) {
|
|
22
|
+
const { exit } = useApp();
|
|
23
|
+
const [displayedLogo, setDisplayedLogo] = useState('');
|
|
24
|
+
const [displayedTagline, setDisplayedTagline] = useState('');
|
|
25
|
+
const [showCursor, setShowCursor] = useState(true);
|
|
26
|
+
const [phase, setPhase] = useState('logo');
|
|
27
|
+
const [showCommands, setShowCommands] = useState(false);
|
|
28
|
+
|
|
29
|
+
// Typing effect for logo (fast)
|
|
30
|
+
useEffect(() => {
|
|
31
|
+
if (phase !== 'logo') return;
|
|
32
|
+
|
|
33
|
+
let i = 0;
|
|
34
|
+
const chars = logo.split('');
|
|
35
|
+
const interval = setInterval(() => {
|
|
36
|
+
if (i < chars.length) {
|
|
37
|
+
setDisplayedLogo(prev => prev + chars[i]);
|
|
38
|
+
i++;
|
|
39
|
+
} else {
|
|
40
|
+
clearInterval(interval);
|
|
41
|
+
setPhase('tagline');
|
|
42
|
+
}
|
|
43
|
+
}, 2);
|
|
44
|
+
|
|
45
|
+
return () => clearInterval(interval);
|
|
46
|
+
}, [phase]);
|
|
47
|
+
|
|
48
|
+
// Typing effect for tagline (slower, more dramatic)
|
|
49
|
+
useEffect(() => {
|
|
50
|
+
if (phase !== 'tagline') return;
|
|
51
|
+
|
|
52
|
+
let i = 0;
|
|
53
|
+
const chars = tagline.split('');
|
|
54
|
+
const interval = setInterval(() => {
|
|
55
|
+
if (i < chars.length) {
|
|
56
|
+
setDisplayedTagline(prev => prev + chars[i]);
|
|
57
|
+
i++;
|
|
58
|
+
} else {
|
|
59
|
+
clearInterval(interval);
|
|
60
|
+
setPhase('commands');
|
|
61
|
+
setShowCommands(true);
|
|
62
|
+
}
|
|
63
|
+
}, 25);
|
|
64
|
+
|
|
65
|
+
return () => clearInterval(interval);
|
|
66
|
+
}, [phase]);
|
|
67
|
+
|
|
68
|
+
// Blinking cursor
|
|
69
|
+
useEffect(() => {
|
|
70
|
+
if (phase === 'commands') {
|
|
71
|
+
setShowCursor(false);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
const interval = setInterval(() => {
|
|
75
|
+
setShowCursor(prev => !prev);
|
|
76
|
+
}, 400);
|
|
77
|
+
return () => clearInterval(interval);
|
|
78
|
+
}, [phase]);
|
|
79
|
+
|
|
80
|
+
// Exit after animation
|
|
81
|
+
useEffect(() => {
|
|
82
|
+
if (showCommands) {
|
|
83
|
+
const timeout = setTimeout(() => exit(), 100);
|
|
84
|
+
return () => clearTimeout(timeout);
|
|
85
|
+
}
|
|
86
|
+
}, [showCommands, exit]);
|
|
87
|
+
|
|
88
|
+
const cursor = showCursor ? '|' : ' ';
|
|
89
|
+
|
|
90
|
+
const elements = [
|
|
91
|
+
React.createElement(Text, { key: 'logo', color: 'cyan', bold: true }, displayedLogo)
|
|
92
|
+
];
|
|
93
|
+
|
|
94
|
+
if (phase !== 'logo') {
|
|
95
|
+
elements.push(
|
|
96
|
+
React.createElement(
|
|
97
|
+
Box,
|
|
98
|
+
{ key: 'tagline-box', marginTop: 0 },
|
|
99
|
+
React.createElement(
|
|
100
|
+
Text,
|
|
101
|
+
{ color: 'magenta', italic: true },
|
|
102
|
+
displayedTagline,
|
|
103
|
+
React.createElement(Text, { color: 'gray' }, cursor)
|
|
104
|
+
)
|
|
105
|
+
)
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (showCommands) {
|
|
110
|
+
elements.push(
|
|
111
|
+
React.createElement(Text, { key: 'divider', color: 'gray' }, '─────────────────────────────────────────')
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
elements.push(
|
|
115
|
+
React.createElement(
|
|
116
|
+
Box,
|
|
117
|
+
{ key: 'commands-box', marginTop: 1, flexDirection: 'column' },
|
|
118
|
+
React.createElement(Text, { color: 'yellow', bold: true }, 'Quick Start:'),
|
|
119
|
+
...commands.map((item, idx) =>
|
|
120
|
+
React.createElement(
|
|
121
|
+
Box,
|
|
122
|
+
{ key: `cmd-${idx}`, marginLeft: 2 },
|
|
123
|
+
React.createElement(Text, { color: 'green' }, `$ ${item.cmd}`),
|
|
124
|
+
React.createElement(Text, { color: 'gray' }, ` ${item.desc}`)
|
|
125
|
+
)
|
|
126
|
+
)
|
|
127
|
+
)
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
elements.push(
|
|
131
|
+
React.createElement(
|
|
132
|
+
Box,
|
|
133
|
+
{ key: 'version-box', marginTop: 1 },
|
|
134
|
+
React.createElement(Text, { color: 'gray' }, `v${version || '0.0.0'}`)
|
|
135
|
+
)
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return React.createElement(
|
|
140
|
+
Box,
|
|
141
|
+
{ flexDirection: 'column', padding: 1 },
|
|
142
|
+
...elements
|
|
143
|
+
);
|
|
144
|
+
}
|