korekt-cli 0.2.0
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/README.md +130 -0
- package/package.json +50 -0
- package/src/config.js +75 -0
- package/src/formatter.js +143 -0
- package/src/git-logic.js +440 -0
- package/src/git-logic.test.js +542 -0
- package/src/index.js +398 -0
package/src/index.js
ADDED
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { program } from 'commander';
|
|
4
|
+
import axios from 'axios';
|
|
5
|
+
import chalk from 'chalk';
|
|
6
|
+
import readline from 'readline';
|
|
7
|
+
import ora from 'ora';
|
|
8
|
+
import { runLocalReview } from './git-logic.js';
|
|
9
|
+
import { getApiKey, setApiKey, getApiEndpoint, setApiEndpoint, getTicketSystem, setTicketSystem } from './config.js';
|
|
10
|
+
import { formatReviewOutput } from './formatter.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Ask for user confirmation before proceeding
|
|
14
|
+
*/
|
|
15
|
+
async function confirmAction(message) {
|
|
16
|
+
const rl = readline.createInterface({
|
|
17
|
+
input: process.stdin,
|
|
18
|
+
output: process.stdout,
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
return new Promise((resolve) => {
|
|
22
|
+
rl.question(message, (answer) => {
|
|
23
|
+
rl.close();
|
|
24
|
+
// Default to 'yes' if the user just presses Enter
|
|
25
|
+
resolve(answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes' || answer === '');
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
program
|
|
31
|
+
.name('kk')
|
|
32
|
+
.description('AI-powered code review CLI - Keep your kode korekt')
|
|
33
|
+
.version('1.0.0')
|
|
34
|
+
.addHelpText('after', `
|
|
35
|
+
Examples:
|
|
36
|
+
$ kk review Review committed changes (auto-detect base)
|
|
37
|
+
$ kk review main Review changes against main branch
|
|
38
|
+
$ kk stg --dry-run Preview staged changes review
|
|
39
|
+
$ kk diff Review unstaged changes
|
|
40
|
+
$ kk all Review all uncommitted changes
|
|
41
|
+
|
|
42
|
+
Common Options:
|
|
43
|
+
--dry-run Show payload without sending to API
|
|
44
|
+
--ticket-system <system> Use specific ticket system (jira or ado)
|
|
45
|
+
|
|
46
|
+
Configuration:
|
|
47
|
+
$ kk config --key YOUR_KEY
|
|
48
|
+
$ kk config --endpoint https://api.korekt.ai/review/local
|
|
49
|
+
$ kk config --ticket-system ado
|
|
50
|
+
`);
|
|
51
|
+
|
|
52
|
+
program
|
|
53
|
+
.command('review')
|
|
54
|
+
.description('Review the changes in the current branch.')
|
|
55
|
+
.argument('[target-branch]', 'The branch to compare against (e.g., main, develop). If not specified, auto-detects fork point.')
|
|
56
|
+
.option('--ticket-system <system>', 'Ticket system to use (jira or ado)')
|
|
57
|
+
.option('--dry-run', 'Show payload without sending to API')
|
|
58
|
+
.option('--ignore <patterns...>', 'Ignore files matching these patterns (e.g., "*.lock" "dist/*")')
|
|
59
|
+
.action(async (targetBranch, options) => {
|
|
60
|
+
const reviewTarget = targetBranch ? `against '${targetBranch}'` : '(auto-detecting fork point)';
|
|
61
|
+
console.log(chalk.blue.bold(`š Starting AI Code Review ${reviewTarget}...`));
|
|
62
|
+
|
|
63
|
+
const apiKey = getApiKey();
|
|
64
|
+
if (!apiKey) {
|
|
65
|
+
console.error(
|
|
66
|
+
chalk.red('API Key not found! Please run `kk config --key YOUR_KEY` first.')
|
|
67
|
+
);
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const apiEndpoint = getApiEndpoint();
|
|
72
|
+
if (!apiEndpoint) {
|
|
73
|
+
console.error(
|
|
74
|
+
chalk.red('API Endpoint not found! Please run `kk config --endpoint YOUR_ENDPOINT` first.')
|
|
75
|
+
);
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Step 1: Determine ticket system to use (or null if not configured)
|
|
80
|
+
const ticketSystem = options.ticketSystem || getTicketSystem() || null;
|
|
81
|
+
|
|
82
|
+
// Validate ticket system
|
|
83
|
+
if (ticketSystem && !['jira', 'ado'].includes(ticketSystem.toLowerCase())) {
|
|
84
|
+
console.error(chalk.red(`Invalid ticket system: ${ticketSystem}`));
|
|
85
|
+
console.error(chalk.gray('Valid options: jira, ado'));
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Step 2: Gather all data using our git logic module
|
|
90
|
+
const payload = await runLocalReview(targetBranch, ticketSystem, options.ignore);
|
|
91
|
+
|
|
92
|
+
if (!payload) {
|
|
93
|
+
console.error(chalk.red('Could not proceed with review due to errors during analysis.'));
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Step 3: Add ticket system to payload if specified
|
|
98
|
+
if (ticketSystem) {
|
|
99
|
+
payload.ticket_system = ticketSystem;
|
|
100
|
+
console.log(chalk.gray(`Using ticket system: ${ticketSystem}`));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Step 4: If dry-run, just show the payload and exit
|
|
104
|
+
if (options.dryRun) {
|
|
105
|
+
console.log(chalk.yellow('\nš Dry Run - Payload that would be sent:\n'));
|
|
106
|
+
|
|
107
|
+
// Create a shortened version for display
|
|
108
|
+
const displayPayload = {
|
|
109
|
+
...payload,
|
|
110
|
+
changed_files: payload.changed_files.map(file => ({
|
|
111
|
+
path: file.path,
|
|
112
|
+
status: file.status,
|
|
113
|
+
...(file.old_path && { old_path: file.old_path }),
|
|
114
|
+
diff: file.diff.length > 500 ? `${file.diff.substring(0, 500)}... [truncated ${file.diff.length - 500} chars]` : file.diff,
|
|
115
|
+
content: file.content.length > 500 ? `${file.content.substring(0, 500)}... [truncated ${file.content.length - 500} chars]` : file.content,
|
|
116
|
+
})),
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
console.log(JSON.stringify(displayPayload, null, 2));
|
|
120
|
+
console.log(chalk.gray('\nš” Run without --dry-run to send to API'));
|
|
121
|
+
console.log(chalk.gray('š” Diffs and content are truncated in dry-run for readability'));
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Step 5: Show summary and ask for confirmation
|
|
126
|
+
console.log(chalk.yellow('\nš Ready to submit for review:\n'));
|
|
127
|
+
console.log(` Branch: ${chalk.cyan(payload.source_branch)}`);
|
|
128
|
+
console.log(` Commits: ${chalk.cyan(payload.commit_messages.length)}`);
|
|
129
|
+
console.log(` Files: ${chalk.cyan(payload.changed_files.length)}\n`);
|
|
130
|
+
|
|
131
|
+
console.log(chalk.bold(' Files to review:'));
|
|
132
|
+
payload.changed_files.forEach(file => {
|
|
133
|
+
const statusColor = {
|
|
134
|
+
'M': chalk.yellow,
|
|
135
|
+
'A': chalk.green,
|
|
136
|
+
'D': chalk.red,
|
|
137
|
+
'R': chalk.blue,
|
|
138
|
+
'C': chalk.cyan,
|
|
139
|
+
}[file.status] || (text => text);
|
|
140
|
+
console.log(` ${statusColor(file.status + ' ' + file.path)}`);
|
|
141
|
+
});
|
|
142
|
+
console.log();
|
|
143
|
+
|
|
144
|
+
const confirmed = await confirmAction(chalk.bold('Proceed with AI review? (Y/n): '));
|
|
145
|
+
|
|
146
|
+
if (!confirmed) {
|
|
147
|
+
console.log(chalk.yellow('Review cancelled.'));
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Step 6: Send the payload to your API
|
|
152
|
+
const spinner = ora('Submitting review to the AI...').start();
|
|
153
|
+
const startTime = Date.now();
|
|
154
|
+
|
|
155
|
+
// Update spinner with elapsed time every second
|
|
156
|
+
const timer = setInterval(() => {
|
|
157
|
+
const elapsed = Math.floor((Date.now() - startTime) / 1000);
|
|
158
|
+
spinner.text = `Submitting review to the AI... ${elapsed}s`;
|
|
159
|
+
}, 1000);
|
|
160
|
+
|
|
161
|
+
try {
|
|
162
|
+
const response = await axios.post(apiEndpoint, payload, {
|
|
163
|
+
headers: {
|
|
164
|
+
'Authorization': `Bearer ${apiKey}`,
|
|
165
|
+
'Content-Type': 'application/json',
|
|
166
|
+
},
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
clearInterval(timer);
|
|
170
|
+
const elapsed = Math.floor((Date.now() - startTime) / 1000);
|
|
171
|
+
spinner.succeed(`Review completed in ${elapsed}s!`);
|
|
172
|
+
|
|
173
|
+
// Step 6: Format and display the results beautifully
|
|
174
|
+
formatReviewOutput(response.data);
|
|
175
|
+
} catch (error) {
|
|
176
|
+
clearInterval(timer);
|
|
177
|
+
const elapsed = Math.floor((Date.now() - startTime) / 1000);
|
|
178
|
+
spinner.fail(`Review failed after ${elapsed}s`);
|
|
179
|
+
console.error(chalk.red('\nā An error occurred during the API request:'));
|
|
180
|
+
if (error.response) {
|
|
181
|
+
console.error(chalk.red('Status:'), error.response.status);
|
|
182
|
+
console.error(chalk.red('Data:'), JSON.stringify(error.response.data, null, 2));
|
|
183
|
+
} else {
|
|
184
|
+
console.error(error.message);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
program
|
|
190
|
+
.command('review-staged')
|
|
191
|
+
.aliases(['stg', 'staged', 'cached'])
|
|
192
|
+
.description('Review staged changes (git diff --cached)')
|
|
193
|
+
.option('--ticket-system <system>', 'Ticket system to use (jira or ado)')
|
|
194
|
+
.option('--dry-run', 'Show payload without sending to API')
|
|
195
|
+
.action(async (options) => {
|
|
196
|
+
console.log(chalk.blue.bold('š Reviewing staged changes...'));
|
|
197
|
+
await reviewUncommitted('staged', options);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
program
|
|
201
|
+
.command('review-unstaged')
|
|
202
|
+
.alias('diff')
|
|
203
|
+
.description('Review unstaged changes (git diff)')
|
|
204
|
+
.option('--ticket-system <system>', 'Ticket system to use (jira or ado)')
|
|
205
|
+
.option('--dry-run', 'Show payload without sending to API')
|
|
206
|
+
.option('--untracked', 'Include untracked files in the review')
|
|
207
|
+
.action(async (options) => {
|
|
208
|
+
console.log(chalk.blue.bold('š Reviewing unstaged changes...'));
|
|
209
|
+
await reviewUncommitted('unstaged', options);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
program
|
|
213
|
+
.command('review-all-uncommitted')
|
|
214
|
+
.alias('all')
|
|
215
|
+
.description('Review all uncommitted changes (staged + unstaged)')
|
|
216
|
+
.option('--ticket-system <system>', 'Ticket system to use (jira or ado)')
|
|
217
|
+
.option('--dry-run', 'Show payload without sending to API')
|
|
218
|
+
.option('--untracked', 'Include untracked files in the review')
|
|
219
|
+
.action(async (options) => {
|
|
220
|
+
console.log(chalk.blue.bold('š Reviewing all uncommitted changes...'));
|
|
221
|
+
await reviewUncommitted('all', options);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
async function reviewUncommitted(mode, options) {
|
|
225
|
+
const apiKey = getApiKey();
|
|
226
|
+
if (!apiKey) {
|
|
227
|
+
console.error(
|
|
228
|
+
chalk.red('API Key not found! Please run `kk config --key YOUR_KEY` first.')
|
|
229
|
+
);
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const apiEndpoint = getApiEndpoint();
|
|
234
|
+
if (!apiEndpoint) {
|
|
235
|
+
console.error(
|
|
236
|
+
chalk.red('API Endpoint not found! Please run `kk config --endpoint YOUR_ENDPOINT` first.')
|
|
237
|
+
);
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const ticketSystem = options.ticketSystem || getTicketSystem() || null;
|
|
242
|
+
|
|
243
|
+
if (ticketSystem && !['jira', 'ado'].includes(ticketSystem.toLowerCase())) {
|
|
244
|
+
console.error(chalk.red(`Invalid ticket system: ${ticketSystem}`));
|
|
245
|
+
console.error(chalk.gray('Valid options: jira, ado'));
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Import the function we'll create
|
|
250
|
+
const { runUncommittedReview } = await import('./git-logic.js');
|
|
251
|
+
const payload = await runUncommittedReview(mode, ticketSystem, options.untracked);
|
|
252
|
+
|
|
253
|
+
if (!payload) {
|
|
254
|
+
// No changes found or error occurred - message already printed by runUncommittedReview
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (ticketSystem) {
|
|
259
|
+
payload.ticket_system = ticketSystem;
|
|
260
|
+
console.log(chalk.gray(`Using ticket system: ${ticketSystem}`));
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if (options.dryRun) {
|
|
264
|
+
console.log(chalk.yellow('\nš Dry Run - Payload that would be sent:\n'));
|
|
265
|
+
|
|
266
|
+
const displayPayload = {
|
|
267
|
+
...payload,
|
|
268
|
+
changed_files: payload.changed_files.map(file => ({
|
|
269
|
+
path: file.path,
|
|
270
|
+
status: file.status,
|
|
271
|
+
...(file.old_path && { old_path: file.old_path }),
|
|
272
|
+
diff: file.diff.length > 500 ? `${file.diff.substring(0, 500)}... [truncated ${file.diff.length - 500} chars]` : file.diff,
|
|
273
|
+
content: file.content.length > 500 ? `${file.content.substring(0, 500)}... [truncated ${file.content.length - 500} chars]` : file.content,
|
|
274
|
+
})),
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
console.log(JSON.stringify(displayPayload, null, 2));
|
|
278
|
+
console.log(chalk.gray('\nš” Run without --dry-run to send to API'));
|
|
279
|
+
console.log(chalk.gray('š” Diffs and content are truncated in dry-run for readability'));
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Show summary and ask for confirmation
|
|
284
|
+
console.log(chalk.yellow('\nš Ready to submit uncommitted changes for review:\n'));
|
|
285
|
+
console.log(chalk.gray(' Comparing against HEAD (last commit)\n'));
|
|
286
|
+
console.log(chalk.bold(' Files to review:'));
|
|
287
|
+
payload.changed_files.forEach(file => {
|
|
288
|
+
const statusColor = {
|
|
289
|
+
'M': chalk.yellow,
|
|
290
|
+
'A': chalk.green,
|
|
291
|
+
'D': chalk.red,
|
|
292
|
+
'R': chalk.blue,
|
|
293
|
+
'C': chalk.cyan,
|
|
294
|
+
}[file.status] || (text => text);
|
|
295
|
+
console.log(` ${statusColor(file.status + ' ' + file.path)}`);
|
|
296
|
+
});
|
|
297
|
+
console.log();
|
|
298
|
+
|
|
299
|
+
const confirmed = await confirmAction(chalk.bold('Proceed with AI review? (Y/n): '));
|
|
300
|
+
|
|
301
|
+
if (!confirmed) {
|
|
302
|
+
console.log(chalk.yellow('Review cancelled.'));
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
const spinner = ora('Submitting review to the AI...').start();
|
|
307
|
+
const startTime = Date.now();
|
|
308
|
+
|
|
309
|
+
// Update spinner with elapsed time every second
|
|
310
|
+
const timer = setInterval(() => {
|
|
311
|
+
const elapsed = Math.floor((Date.now() - startTime) / 1000);
|
|
312
|
+
spinner.text = `Submitting review to the AI... ${elapsed}s`;
|
|
313
|
+
}, 1000);
|
|
314
|
+
|
|
315
|
+
try {
|
|
316
|
+
const response = await axios.post(apiEndpoint, payload, {
|
|
317
|
+
headers: {
|
|
318
|
+
'Authorization': `Bearer ${apiKey}`,
|
|
319
|
+
'Content-Type': 'application/json',
|
|
320
|
+
},
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
clearInterval(timer);
|
|
324
|
+
const elapsed = Math.floor((Date.now() - startTime) / 1000);
|
|
325
|
+
spinner.succeed(`Review completed in ${elapsed}s!`);
|
|
326
|
+
|
|
327
|
+
formatReviewOutput(response.data);
|
|
328
|
+
} catch (error) {
|
|
329
|
+
clearInterval(timer);
|
|
330
|
+
const elapsed = Math.floor((Date.now() - startTime) / 1000);
|
|
331
|
+
spinner.fail(`Review failed after ${elapsed}s`);
|
|
332
|
+
console.error(chalk.red('\nā An error occurred during the API request:'));
|
|
333
|
+
if (error.response) {
|
|
334
|
+
console.error(chalk.red('Status:'), error.response.status);
|
|
335
|
+
console.error(chalk.red('Data:'), JSON.stringify(error.response.data, null, 2));
|
|
336
|
+
} else {
|
|
337
|
+
console.error(error.message);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
program
|
|
343
|
+
.command('config')
|
|
344
|
+
.description('Configure API settings')
|
|
345
|
+
.option('--key <key>', 'Your API key')
|
|
346
|
+
.option('--endpoint <endpoint>', 'Your API endpoint URL')
|
|
347
|
+
.option('--ticket-system <system>', 'Ticket system (jira, ado)')
|
|
348
|
+
.option('--show', 'Show current configuration')
|
|
349
|
+
.action((options) => {
|
|
350
|
+
// Show current config if --show flag is used
|
|
351
|
+
if (options.show) {
|
|
352
|
+
const apiKey = getApiKey();
|
|
353
|
+
const apiEndpoint = getApiEndpoint();
|
|
354
|
+
const ticketSystem = getTicketSystem();
|
|
355
|
+
|
|
356
|
+
console.log(chalk.bold('\nCurrent Configuration:\n'));
|
|
357
|
+
console.log(` API Key: ${apiKey ? chalk.green('ā Set') : chalk.red('ā Not set')}`);
|
|
358
|
+
console.log(` API Endpoint: ${apiEndpoint ? chalk.cyan(apiEndpoint) : chalk.red('ā Not set')}`);
|
|
359
|
+
console.log(` Ticket System: ${ticketSystem ? chalk.cyan(ticketSystem) : chalk.gray('Not configured')}\n`);
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
if (options.key) {
|
|
364
|
+
setApiKey(options.key);
|
|
365
|
+
console.log(chalk.green('ā API Key saved successfully!'));
|
|
366
|
+
}
|
|
367
|
+
if (options.endpoint) {
|
|
368
|
+
setApiEndpoint(options.endpoint);
|
|
369
|
+
console.log(chalk.green('ā API Endpoint saved successfully!'));
|
|
370
|
+
}
|
|
371
|
+
if (options.ticketSystem !== undefined) {
|
|
372
|
+
if (options.ticketSystem === '') {
|
|
373
|
+
// Clear ticket system
|
|
374
|
+
setTicketSystem(null);
|
|
375
|
+
console.log(chalk.green('ā Ticket System cleared!'));
|
|
376
|
+
} else {
|
|
377
|
+
// Validate ticket system
|
|
378
|
+
const validSystems = ['jira', 'ado'];
|
|
379
|
+
if (!validSystems.includes(options.ticketSystem.toLowerCase())) {
|
|
380
|
+
console.error(chalk.red(`Invalid ticket system: ${options.ticketSystem}`));
|
|
381
|
+
console.error(chalk.gray(`Valid options: ${validSystems.join(', ')}`));
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
setTicketSystem(options.ticketSystem);
|
|
385
|
+
console.log(chalk.green('ā Ticket System saved successfully!'));
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
if (!options.key && !options.endpoint && options.ticketSystem === undefined && !options.show) {
|
|
389
|
+
console.log(chalk.yellow('Please provide at least one configuration option.'));
|
|
390
|
+
console.log('\nUsage:');
|
|
391
|
+
console.log(' kk config --key YOUR_API_KEY');
|
|
392
|
+
console.log(' kk config --endpoint https://api.korekt.ai/review/local');
|
|
393
|
+
console.log(' kk config --ticket-system jira');
|
|
394
|
+
console.log(' kk config --show (view current configuration)');
|
|
395
|
+
}
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
program.parse();
|