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/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();