korekt-cli 0.11.2 ā 0.12.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/package.json +1 -1
- package/src/formatter.js +6 -1
- package/src/index.js +95 -0
- package/src/index.test.js +80 -0
package/package.json
CHANGED
package/src/formatter.js
CHANGED
|
@@ -76,7 +76,7 @@ function toAbsolutePath(filePath) {
|
|
|
76
76
|
* @param {Object} data - The API response data
|
|
77
77
|
*/
|
|
78
78
|
export function formatReviewOutput(data) {
|
|
79
|
-
const { review, summary, change_classification: changeClassification } = data.data;
|
|
79
|
+
const { review, summary, change_classification: changeClassification, model } = data.data;
|
|
80
80
|
|
|
81
81
|
console.log(chalk.bold.blue('š¤ Automated Code Review Results\n'));
|
|
82
82
|
|
|
@@ -162,4 +162,9 @@ export function formatReviewOutput(data) {
|
|
|
162
162
|
console.log(); // Add a blank line for spacing
|
|
163
163
|
});
|
|
164
164
|
}
|
|
165
|
+
|
|
166
|
+
// Footer with model info
|
|
167
|
+
if (model) {
|
|
168
|
+
console.log(chalk.gray(`Analyzed with ${model}`));
|
|
169
|
+
}
|
|
165
170
|
}
|
package/src/index.js
CHANGED
|
@@ -72,6 +72,57 @@ async function confirmAction(message) {
|
|
|
72
72
|
});
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
+
/**
|
|
76
|
+
* Available Gemini models for code review
|
|
77
|
+
*/
|
|
78
|
+
const GEMINI_MODELS = [
|
|
79
|
+
{ value: 'gemini-2.5-pro', label: 'gemini-2.5-pro (high quality)' },
|
|
80
|
+
{ value: 'gemini-2.5-flash', label: 'gemini-2.5-flash (avoid, the worst quality)' },
|
|
81
|
+
{ value: 'gemini-3-pro-preview', label: 'gemini-3-pro-preview (experimental - the best model)' },
|
|
82
|
+
{
|
|
83
|
+
value: 'gemini-3-flash-preview',
|
|
84
|
+
label: 'gemini-3-flash-preview (experimental - the most efficient model)',
|
|
85
|
+
},
|
|
86
|
+
];
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Prompt user to select a Gemini model
|
|
90
|
+
* @returns {Promise<string>} - Selected model name
|
|
91
|
+
* @throws {Error} - If not running in interactive terminal
|
|
92
|
+
*/
|
|
93
|
+
async function selectModel() {
|
|
94
|
+
// Check if running in interactive terminal
|
|
95
|
+
if (!process.stdin.isTTY) {
|
|
96
|
+
log(chalk.red('Error: --model flag requires a value in non-interactive environments.'));
|
|
97
|
+
log(chalk.gray('Example: kk review --model=gemini-2.5-pro'));
|
|
98
|
+
process.exit(1);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const rl = readline.createInterface({
|
|
102
|
+
input: process.stdin,
|
|
103
|
+
output: process.stderr,
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
log(chalk.yellow('\nSelect Gemini model:\n'));
|
|
107
|
+
GEMINI_MODELS.forEach((model, index) => {
|
|
108
|
+
log(` ${index + 1}. ${model.label}`);
|
|
109
|
+
});
|
|
110
|
+
log('');
|
|
111
|
+
|
|
112
|
+
return new Promise((resolvePromise) => {
|
|
113
|
+
rl.question(chalk.bold('Enter number (1-4): '), (answer) => {
|
|
114
|
+
rl.close();
|
|
115
|
+
const num = parseInt(answer, 10);
|
|
116
|
+
if (num >= 1 && num <= GEMINI_MODELS.length) {
|
|
117
|
+
resolvePromise(GEMINI_MODELS[num - 1].value);
|
|
118
|
+
} else {
|
|
119
|
+
log(chalk.red('Invalid selection. Please run the command again and enter 1-4.'));
|
|
120
|
+
process.exit(1);
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
75
126
|
/**
|
|
76
127
|
* Run the CI integration script to post comments
|
|
77
128
|
* @param {string} provider - CI provider (github, azure, bitbucket)
|
|
@@ -128,6 +179,8 @@ program
|
|
|
128
179
|
Examples:
|
|
129
180
|
$ kk review Review committed changes (auto-detect base)
|
|
130
181
|
$ kk review main Review changes against main branch
|
|
182
|
+
$ kk review --model Select Gemini model interactively
|
|
183
|
+
$ kk review -m gemini-2.5-pro Use specific model
|
|
131
184
|
$ kk stg --dry-run Preview staged changes review
|
|
132
185
|
$ kk diff Review unstaged changes
|
|
133
186
|
$ kk review main --json Output raw JSON (for CI/CD integration)
|
|
@@ -137,6 +190,7 @@ Common Options:
|
|
|
137
190
|
--dry-run Show payload without sending to API
|
|
138
191
|
--json Output raw API response as JSON
|
|
139
192
|
--comment Post review results as PR comments
|
|
193
|
+
-m, --model [name] Gemini model (picker if no value)
|
|
140
194
|
|
|
141
195
|
Configuration:
|
|
142
196
|
$ kk config --key YOUR_KEY
|
|
@@ -159,6 +213,7 @@ program
|
|
|
159
213
|
.option('--json', 'Output raw API response as JSON')
|
|
160
214
|
.option('--comment', 'Post review results as PR comments (auto-detects CI provider)')
|
|
161
215
|
.option('--post-ticket', 'Post review results to linked ticket (e.g., JIRA)')
|
|
216
|
+
.option('-m, --model [name]', 'Gemini model to use (interactive picker if no value provided)')
|
|
162
217
|
.action(async (targetBranch, options) => {
|
|
163
218
|
const reviewTarget = targetBranch ? `against '${targetBranch}'` : '(auto-detecting fork point)';
|
|
164
219
|
|
|
@@ -179,6 +234,15 @@ program
|
|
|
179
234
|
process.exit(1);
|
|
180
235
|
}
|
|
181
236
|
|
|
237
|
+
// Handle model selection: if --model without value, show picker
|
|
238
|
+
let selectedModel = null;
|
|
239
|
+
if (options.model === true) {
|
|
240
|
+
selectedModel = await selectModel();
|
|
241
|
+
log(chalk.green(`Using model: ${selectedModel}\n`));
|
|
242
|
+
} else if (typeof options.model === 'string') {
|
|
243
|
+
selectedModel = options.model;
|
|
244
|
+
}
|
|
245
|
+
|
|
182
246
|
// Fetch file rules config from API
|
|
183
247
|
const fileRulesConfig = await fetchFileRulesConfig(apiEndpoint, apiKey);
|
|
184
248
|
|
|
@@ -206,6 +270,11 @@ program
|
|
|
206
270
|
changed_files: payload.changed_files.map((file) => truncateFileData(file)),
|
|
207
271
|
};
|
|
208
272
|
|
|
273
|
+
// Add model if specified
|
|
274
|
+
if (selectedModel) {
|
|
275
|
+
displayPayload.model = selectedModel;
|
|
276
|
+
}
|
|
277
|
+
|
|
209
278
|
log(JSON.stringify(displayPayload, null, 2));
|
|
210
279
|
log(chalk.gray('\nš” Run without --dry-run to send to API'));
|
|
211
280
|
log(chalk.gray('š” Diffs and content are truncated in dry-run for readability'));
|
|
@@ -255,6 +324,11 @@ program
|
|
|
255
324
|
payload.post_to_ticket = true;
|
|
256
325
|
}
|
|
257
326
|
|
|
327
|
+
// Add model if specified
|
|
328
|
+
if (selectedModel) {
|
|
329
|
+
payload.model = selectedModel;
|
|
330
|
+
}
|
|
331
|
+
|
|
258
332
|
try {
|
|
259
333
|
const response = await axios.post(apiEndpoint, payload, {
|
|
260
334
|
headers: {
|
|
@@ -328,6 +402,7 @@ program
|
|
|
328
402
|
.description('Review staged changes (git diff --cached)')
|
|
329
403
|
.option('--dry-run', 'Show payload without sending to API')
|
|
330
404
|
.option('--json', 'Output raw API response as JSON')
|
|
405
|
+
.option('-m, --model [name]', 'Gemini model to use (interactive picker if no value provided)')
|
|
331
406
|
.action(async (options) => {
|
|
332
407
|
log(chalk.blue.bold('š Reviewing staged changes...'));
|
|
333
408
|
await reviewUncommitted('staged', options);
|
|
@@ -339,6 +414,7 @@ program
|
|
|
339
414
|
.description('Review unstaged changes (git diff)')
|
|
340
415
|
.option('--dry-run', 'Show payload without sending to API')
|
|
341
416
|
.option('--json', 'Output raw API response as JSON')
|
|
417
|
+
.option('-m, --model [name]', 'Gemini model to use (interactive picker if no value provided)')
|
|
342
418
|
.action(async (options) => {
|
|
343
419
|
log(chalk.blue.bold('š Reviewing unstaged changes...'));
|
|
344
420
|
await reviewUncommitted('unstaged', options);
|
|
@@ -359,6 +435,15 @@ async function reviewUncommitted(mode, options) {
|
|
|
359
435
|
process.exit(1);
|
|
360
436
|
}
|
|
361
437
|
|
|
438
|
+
// Handle model selection: if --model without value, show picker
|
|
439
|
+
let selectedModel = null;
|
|
440
|
+
if (options.model === true) {
|
|
441
|
+
selectedModel = await selectModel();
|
|
442
|
+
log(chalk.green(`Using model: ${selectedModel}\n`));
|
|
443
|
+
} else if (typeof options.model === 'string') {
|
|
444
|
+
selectedModel = options.model;
|
|
445
|
+
}
|
|
446
|
+
|
|
362
447
|
// Fetch file rules config from API
|
|
363
448
|
const fileRulesConfig = await fetchFileRulesConfig(apiEndpoint, apiKey);
|
|
364
449
|
|
|
@@ -378,6 +463,11 @@ async function reviewUncommitted(mode, options) {
|
|
|
378
463
|
changed_files: payload.changed_files.map((file) => truncateFileData(file)),
|
|
379
464
|
};
|
|
380
465
|
|
|
466
|
+
// Add model if specified
|
|
467
|
+
if (selectedModel) {
|
|
468
|
+
displayPayload.model = selectedModel;
|
|
469
|
+
}
|
|
470
|
+
|
|
381
471
|
log(JSON.stringify(displayPayload, null, 2));
|
|
382
472
|
log(chalk.gray('\nš” Run without --dry-run to send to API'));
|
|
383
473
|
log(chalk.gray('š” Diffs and content are truncated in dry-run for readability'));
|
|
@@ -410,6 +500,11 @@ async function reviewUncommitted(mode, options) {
|
|
|
410
500
|
}
|
|
411
501
|
}
|
|
412
502
|
|
|
503
|
+
// Add model if specified
|
|
504
|
+
if (selectedModel) {
|
|
505
|
+
payload.model = selectedModel;
|
|
506
|
+
}
|
|
507
|
+
|
|
413
508
|
const spinner = ora('Submitting review to the AI...').start();
|
|
414
509
|
const startTime = Date.now();
|
|
415
510
|
|
package/src/index.test.js
CHANGED
|
@@ -469,6 +469,86 @@ describe('--comment flag behavior', () => {
|
|
|
469
469
|
});
|
|
470
470
|
});
|
|
471
471
|
|
|
472
|
+
describe('--model flag behavior', () => {
|
|
473
|
+
describe('model selection logic', () => {
|
|
474
|
+
it('should treat --model without value as true (picker mode)', () => {
|
|
475
|
+
const options = { model: true };
|
|
476
|
+
|
|
477
|
+
// When --model is used without a value, Commander sets it to true
|
|
478
|
+
const shouldShowPicker = options.model === true;
|
|
479
|
+
const hasDirectValue = typeof options.model === 'string';
|
|
480
|
+
|
|
481
|
+
expect(shouldShowPicker).toBe(true);
|
|
482
|
+
expect(hasDirectValue).toBe(false);
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
it('should treat --model=value as string (direct mode)', () => {
|
|
486
|
+
const options = { model: 'gemini-2.5-flash' };
|
|
487
|
+
|
|
488
|
+
const shouldShowPicker = options.model === true;
|
|
489
|
+
const hasDirectValue = typeof options.model === 'string';
|
|
490
|
+
|
|
491
|
+
expect(shouldShowPicker).toBe(false);
|
|
492
|
+
expect(hasDirectValue).toBe(true);
|
|
493
|
+
expect(options.model).toBe('gemini-2.5-flash');
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
it('should not include model when flag is not used', () => {
|
|
497
|
+
const options = {};
|
|
498
|
+
|
|
499
|
+
const shouldIncludeModel = options.model !== undefined;
|
|
500
|
+
|
|
501
|
+
expect(shouldIncludeModel).toBe(false);
|
|
502
|
+
});
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
describe('payload model field', () => {
|
|
506
|
+
it('should add model to payload when specified', () => {
|
|
507
|
+
const payload = {
|
|
508
|
+
repo_url: 'https://github.com/user/repo',
|
|
509
|
+
changed_files: [],
|
|
510
|
+
};
|
|
511
|
+
|
|
512
|
+
const selectedModel = 'gemini-2.5-flash';
|
|
513
|
+
if (selectedModel) {
|
|
514
|
+
payload.model = selectedModel;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
expect(payload.model).toBe('gemini-2.5-flash');
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
it('should not add model to payload when not specified', () => {
|
|
521
|
+
const payload = {
|
|
522
|
+
repo_url: 'https://github.com/user/repo',
|
|
523
|
+
changed_files: [],
|
|
524
|
+
};
|
|
525
|
+
|
|
526
|
+
const selectedModel = null;
|
|
527
|
+
if (selectedModel) {
|
|
528
|
+
payload.model = selectedModel;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
expect(payload.model).toBeUndefined();
|
|
532
|
+
});
|
|
533
|
+
});
|
|
534
|
+
|
|
535
|
+
describe('available models', () => {
|
|
536
|
+
it('should have valid model values', () => {
|
|
537
|
+
const validModels = [
|
|
538
|
+
'gemini-2.5-pro',
|
|
539
|
+
'gemini-2.5-flash',
|
|
540
|
+
'gemini-3-pro-preview',
|
|
541
|
+
'gemini-3-flash-preview',
|
|
542
|
+
];
|
|
543
|
+
|
|
544
|
+
// Test that all expected models are valid Gemini model names
|
|
545
|
+
validModels.forEach((model) => {
|
|
546
|
+
expect(model).toMatch(/^gemini-/);
|
|
547
|
+
});
|
|
548
|
+
});
|
|
549
|
+
});
|
|
550
|
+
});
|
|
551
|
+
|
|
472
552
|
describe('getPrUrl', () => {
|
|
473
553
|
const originalEnv = process.env;
|
|
474
554
|
|