korekt-cli 0.9.6 → 0.10.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "korekt-cli",
3
- "version": "0.9.6",
3
+ "version": "0.10.0",
4
4
  "description": "AI-powered code review CLI - Keep your kode korekt",
5
5
  "main": "src/index.js",
6
6
  "bin": {
package/scripts/azure.sh CHANGED
@@ -232,6 +232,9 @@ post_review_thread() {
232
232
  TOTAL_ISSUES=$(jq -r '.data.summary.total_issues // 0' "$RESULTS_FILE")
233
233
  TOTAL_PRAISES=$(jq -r '.data.summary.total_praises // 0' "$RESULTS_FILE")
234
234
  CRITICAL_ISSUES=$(jq -r '.data.summary.critical // 0' "$RESULTS_FILE")
235
+ CHANGE_SUMMARY=$(jq -r '.data.change_classification?.summary // ""' "$RESULTS_FILE")
236
+ CHANGE_INTENT=$(jq -r '.data.change_classification?.intent // ""' "$RESULTS_FILE")
237
+ CHANGE_ASPECTS=$(jq -r '.data.change_classification?.aspects // [] | join(", ")' "$RESULTS_FILE")
235
238
 
236
239
  # Post inline comments for issues (excluding low severity)
237
240
  if [ "$TOTAL_ISSUES" -gt 0 ] && [ "$POST_INLINE_COMMENTS" = "true" ]; then
@@ -318,6 +321,20 @@ else
318
321
  echo "šŸ¤– **Automated Code Review Results**" >> "$COMMENT_FILE"
319
322
  echo "" >> "$COMMENT_FILE"
320
323
 
324
+ # Change Summary section
325
+ if [ -n "$CHANGE_SUMMARY" ]; then
326
+ echo "### šŸ“ Change Summary" >> "$COMMENT_FILE"
327
+ echo "$CHANGE_SUMMARY" >> "$COMMENT_FILE"
328
+ echo "" >> "$COMMENT_FILE"
329
+
330
+ # Build metadata line
331
+ META=""
332
+ [ -n "$CHANGE_INTENT" ] && META="Intent: $CHANGE_INTENT"
333
+ [ -n "$CHANGE_ASPECTS" ] && { [ -n "$META" ] && META="$META | "; META="${META}Aspects: $CHANGE_ASPECTS"; }
334
+ [ -n "$META" ] && echo "_${META}_" >> "$COMMENT_FILE"
335
+ echo "" >> "$COMMENT_FILE"
336
+ fi
337
+
321
338
  # Praises section
322
339
  if [ "$TOTAL_PRAISES" -gt 0 ]; then
323
340
  echo "### ✨ Praises ($TOTAL_PRAISES)" >> "$COMMENT_FILE"
@@ -205,6 +205,9 @@ post_inline_comment() {
205
205
  TOTAL_ISSUES=$(jq -r '.data.summary.total_issues // 0' "$RESULTS_FILE")
206
206
  TOTAL_PRAISES=$(jq -r '.data.summary.total_praises // 0' "$RESULTS_FILE")
207
207
  CRITICAL_ISSUES=$(jq -r '.data.summary.critical // 0' "$RESULTS_FILE")
208
+ CHANGE_SUMMARY=$(jq -r '.data.change_classification?.summary // ""' "$RESULTS_FILE")
209
+ CHANGE_INTENT=$(jq -r '.data.change_classification?.intent // ""' "$RESULTS_FILE")
210
+ CHANGE_ASPECTS=$(jq -r '.data.change_classification?.aspects // [] | join(", ")' "$RESULTS_FILE")
208
211
 
209
212
  # Post inline comments for issues (excluding low severity)
210
213
  if [ "$TOTAL_ISSUES" -gt 0 ] && [ "$POST_INLINE_COMMENTS" = "true" ]; then
@@ -288,6 +291,20 @@ else
288
291
  echo "šŸ¤– **Automated Code Review Results**" >> "$COMMENT_FILE"
289
292
  echo "" >> "$COMMENT_FILE"
290
293
 
294
+ # Change Summary section
295
+ if [ -n "$CHANGE_SUMMARY" ]; then
296
+ echo "### šŸ“ Change Summary" >> "$COMMENT_FILE"
297
+ echo "$CHANGE_SUMMARY" >> "$COMMENT_FILE"
298
+ echo "" >> "$COMMENT_FILE"
299
+
300
+ # Build metadata line
301
+ META=""
302
+ [ -n "$CHANGE_INTENT" ] && META="Intent: $CHANGE_INTENT"
303
+ [ -n "$CHANGE_ASPECTS" ] && { [ -n "$META" ] && META="$META | "; META="${META}Aspects: $CHANGE_ASPECTS"; }
304
+ [ -n "$META" ] && echo "_${META}_" >> "$COMMENT_FILE"
305
+ echo "" >> "$COMMENT_FILE"
306
+ fi
307
+
291
308
  # Praises section
292
309
  if [ "$TOTAL_PRAISES" -gt 0 ]; then
293
310
  echo "### ✨ Praises ($TOTAL_PRAISES)" >> "$COMMENT_FILE"
package/scripts/github.sh CHANGED
@@ -204,6 +204,9 @@ post_review_comment() {
204
204
  TOTAL_ISSUES=$(jq -r '.data.summary.total_issues // 0' "$RESULTS_FILE")
205
205
  TOTAL_PRAISES=$(jq -r '.data.summary.total_praises // 0' "$RESULTS_FILE")
206
206
  CRITICAL_ISSUES=$(jq -r '.data.summary.critical // 0' "$RESULTS_FILE")
207
+ CHANGE_SUMMARY=$(jq -r '.data.change_classification?.summary // ""' "$RESULTS_FILE")
208
+ CHANGE_INTENT=$(jq -r '.data.change_classification?.intent // ""' "$RESULTS_FILE")
209
+ CHANGE_ASPECTS=$(jq -r '.data.change_classification?.aspects // [] | join(", ")' "$RESULTS_FILE")
207
210
 
208
211
  # Post inline comments for issues
209
212
  if [ "$TOTAL_ISSUES" -gt 0 ]; then
@@ -287,6 +290,20 @@ else
287
290
  echo "šŸ¤– **Automated Code Review Results**" >> "$COMMENT_FILE"
288
291
  echo "" >> "$COMMENT_FILE"
289
292
 
293
+ # Change Summary section
294
+ if [ -n "$CHANGE_SUMMARY" ]; then
295
+ echo "### šŸ“ Change Summary" >> "$COMMENT_FILE"
296
+ echo "$CHANGE_SUMMARY" >> "$COMMENT_FILE"
297
+ echo "" >> "$COMMENT_FILE"
298
+
299
+ # Build metadata line
300
+ META=""
301
+ [ -n "$CHANGE_INTENT" ] && META="Intent: $CHANGE_INTENT"
302
+ [ -n "$CHANGE_ASPECTS" ] && { [ -n "$META" ] && META="$META | "; META="${META}Aspects: $CHANGE_ASPECTS"; }
303
+ [ -n "$META" ] && echo "_${META}_" >> "$COMMENT_FILE"
304
+ echo "" >> "$COMMENT_FILE"
305
+ fi
306
+
290
307
  # Praises section
291
308
  if [ "$TOTAL_PRAISES" -gt 0 ]; then
292
309
  echo "### ✨ Praises ($TOTAL_PRAISES)" >> "$COMMENT_FILE"
package/src/formatter.js CHANGED
@@ -76,10 +76,28 @@ function toAbsolutePath(filePath) {
76
76
  * @param {Object} data - The API response data
77
77
  */
78
78
  export function formatReviewOutput(data) {
79
- const { review, summary } = data.data;
79
+ const { review, summary, change_classification: changeClassification } = data.data;
80
80
 
81
81
  console.log(chalk.bold.blue('šŸ¤– Automated Code Review Results\n'));
82
82
 
83
+ // --- Change Summary Section ---
84
+ if (changeClassification && changeClassification.summary) {
85
+ console.log(chalk.bold.cyan('šŸ“ Change Summary\n'));
86
+ console.log(` ${changeClassification.summary}\n`);
87
+
88
+ const meta = [];
89
+ if (changeClassification.intent) {
90
+ meta.push(`Intent: ${changeClassification.intent}`);
91
+ }
92
+ if (changeClassification.aspects && changeClassification.aspects.length > 0) {
93
+ meta.push(`Aspects: ${changeClassification.aspects.join(', ')}`);
94
+ }
95
+ if (meta.length > 0) {
96
+ console.log(chalk.gray(` ${meta.join(' | ')}`));
97
+ }
98
+ console.log(); // Spacing after change summary
99
+ }
100
+
83
101
  // --- Praises Section ---
84
102
  if (review && review.praises && review.praises.length > 0) {
85
103
  console.log(chalk.bold.magenta(`✨ Praises (${summary.total_praises})`));
@@ -0,0 +1,96 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
+ import { formatReviewOutput } from './formatter.js';
3
+
4
+ describe('formatReviewOutput', () => {
5
+ let consoleSpy;
6
+
7
+ beforeEach(() => {
8
+ consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
9
+ });
10
+
11
+ afterEach(() => {
12
+ vi.restoreAllMocks();
13
+ });
14
+
15
+ describe('change_classification output', () => {
16
+ it('should display change summary when change_classification.summary is present', () => {
17
+ const data = {
18
+ data: {
19
+ review: { issues: [], praises: [] },
20
+ summary: { total_issues: 0, total_praises: 0 },
21
+ change_classification: {
22
+ intent: 'fix',
23
+ aspects: ['security', 'tests'],
24
+ summary: 'Fix authentication bypass vulnerability',
25
+ },
26
+ },
27
+ };
28
+
29
+ formatReviewOutput(data);
30
+
31
+ const allCalls = consoleSpy.mock.calls.map((call) => call[0]);
32
+ expect(allCalls.some((call) => call.includes('Change Summary'))).toBe(true);
33
+ expect(
34
+ allCalls.some((call) => call.includes('Fix authentication bypass vulnerability'))
35
+ ).toBe(true);
36
+ expect(allCalls.some((call) => call.includes('Intent: fix'))).toBe(true);
37
+ expect(allCalls.some((call) => call.includes('Aspects: security, tests'))).toBe(true);
38
+ });
39
+
40
+ it('should not display change_classification section when summary is missing', () => {
41
+ const data = {
42
+ data: {
43
+ review: { issues: [], praises: [] },
44
+ summary: { total_issues: 0, total_praises: 0 },
45
+ change_classification: null,
46
+ },
47
+ };
48
+
49
+ formatReviewOutput(data);
50
+
51
+ const allCalls = consoleSpy.mock.calls.map((call) => call[0]);
52
+ expect(allCalls.some((call) => call && call.includes('Change Summary'))).toBe(false);
53
+ });
54
+
55
+ it('should handle change_classification with only summary (no intent/aspects)', () => {
56
+ const data = {
57
+ data: {
58
+ review: { issues: [], praises: [] },
59
+ summary: { total_issues: 0, total_praises: 0 },
60
+ change_classification: {
61
+ summary: 'Add new feature',
62
+ },
63
+ },
64
+ };
65
+
66
+ formatReviewOutput(data);
67
+
68
+ const allCalls = consoleSpy.mock.calls.map((call) => call[0]);
69
+ expect(allCalls.some((call) => call.includes('Change Summary'))).toBe(true);
70
+ expect(allCalls.some((call) => call.includes('Add new feature'))).toBe(true);
71
+ // Should not have metadata line when no intent/aspects
72
+ expect(allCalls.some((call) => call && call.includes('Intent:'))).toBe(false);
73
+ });
74
+
75
+ it('should handle change_classification with empty aspects array', () => {
76
+ const data = {
77
+ data: {
78
+ review: { issues: [], praises: [] },
79
+ summary: { total_issues: 0, total_praises: 0 },
80
+ change_classification: {
81
+ intent: 'feature',
82
+ aspects: [],
83
+ summary: 'Add user dashboard',
84
+ },
85
+ },
86
+ };
87
+
88
+ formatReviewOutput(data);
89
+
90
+ const allCalls = consoleSpy.mock.calls.map((call) => call[0]);
91
+ expect(allCalls.some((call) => call.includes('Add user dashboard'))).toBe(true);
92
+ expect(allCalls.some((call) => call && call.includes('Intent: feature'))).toBe(true);
93
+ expect(allCalls.some((call) => call && call.includes('Aspects:'))).toBe(false);
94
+ });
95
+ });
96
+ });
@@ -259,6 +259,64 @@ describe('runLocalReview - branch fetching', () => {
259
259
  });
260
260
  });
261
261
 
262
+ describe('runLocalReview - no files changed', () => {
263
+ beforeEach(() => {
264
+ vi.mock('execa');
265
+ vi.spyOn(console, 'error').mockImplementation(() => {});
266
+ });
267
+
268
+ afterEach(() => {
269
+ vi.restoreAllMocks();
270
+ });
271
+
272
+ it('should return payload with empty changed_files when diff has no files', async () => {
273
+ vi.mocked(execa).mockImplementation(async (cmd, args) => {
274
+ const command = [cmd, ...args].join(' ');
275
+
276
+ if (command.includes('remote get-url origin')) {
277
+ return { stdout: 'https://github.com/user/repo.git' };
278
+ }
279
+ if (command.includes('rev-parse --abbrev-ref HEAD')) {
280
+ return { stdout: 'feature-branch' };
281
+ }
282
+ if (command === 'git rev-parse HEAD') {
283
+ return { stdout: 'abc123def456789' };
284
+ }
285
+ if (command.includes('rev-parse --show-toplevel')) {
286
+ return { stdout: '/path/to/repo' };
287
+ }
288
+ if (command.includes('rev-parse --verify main')) {
289
+ return { stdout: 'commit-hash' };
290
+ }
291
+ if (command === 'git fetch origin main') {
292
+ return { stdout: '' };
293
+ }
294
+ if (command.includes('merge-base origin/main HEAD')) {
295
+ return { stdout: 'abc123' };
296
+ }
297
+ if (command.includes('log --no-merges --pretty=%B---EOC---')) {
298
+ return { stdout: 'feat: some commit---EOC---' };
299
+ }
300
+ if (command.includes('log --no-merges --format=%ae|%an')) {
301
+ return { stdout: 'user@example.com|User Name' };
302
+ }
303
+ // No files changed
304
+ if (command.includes('diff --name-status')) {
305
+ return { stdout: '' };
306
+ }
307
+
308
+ return { stdout: '' };
309
+ });
310
+
311
+ const result = await runLocalReview('main');
312
+
313
+ expect(result).not.toBeNull();
314
+ expect(result.changed_files).toEqual([]);
315
+ expect(result.source_branch).toBe('feature-branch');
316
+ expect(result.destination_branch).toBe('main');
317
+ });
318
+ });
319
+
262
320
  describe('runLocalReview - fork point detection', () => {
263
321
  beforeEach(() => {
264
322
  vi.mock('execa');
package/src/index.js CHANGED
@@ -165,6 +165,12 @@ program
165
165
  process.exit(1);
166
166
  }
167
167
 
168
+ // Exit successfully if no files to review (common in CI when PR has no code changes)
169
+ if (payload.changed_files.length === 0) {
170
+ log(chalk.green('āœ“ No files changed to review. Exiting successfully.'));
171
+ return;
172
+ }
173
+
168
174
  // If dry-run, just show the payload and exit
169
175
  if (options.dryRun) {
170
176
  log(chalk.yellow('\nšŸ“‹ Dry Run - Payload that would be sent:\n'));