korekt-cli 0.8.2 → 0.8.4

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.8.2",
3
+ "version": "0.8.4",
4
4
  "description": "AI-powered code review CLI - Keep your kode korekt",
5
5
  "main": "src/index.js",
6
6
  "bin": {
package/src/git-logic.js CHANGED
@@ -98,6 +98,30 @@ export function shouldIgnoreFile(filePath, patterns) {
98
98
  return false;
99
99
  }
100
100
 
101
+ /**
102
+ * Calculate total changed lines (additions + deletions) from changed files
103
+ * @param {Array} changedFiles - Array of file objects with diff property
104
+ * @returns {number} - Total number of changed lines
105
+ */
106
+ export function calculateChangedLines(changedFiles) {
107
+ let changedLines = 0;
108
+ for (const file of changedFiles) {
109
+ if (file.diff) {
110
+ const lines = file.diff.split('\n');
111
+ for (const line of lines) {
112
+ if (
113
+ (line.startsWith('+') || line.startsWith('-')) &&
114
+ !line.startsWith('+++') &&
115
+ !line.startsWith('---')
116
+ ) {
117
+ changedLines++;
118
+ }
119
+ }
120
+ }
121
+ }
122
+ return changedLines;
123
+ }
124
+
101
125
  /**
102
126
  * Helper function to parse the complex output of git diff --name-status
103
127
  */
@@ -219,6 +243,7 @@ export async function runUncommittedReview(mode = 'unstaged') {
219
243
  commit_messages: [], // No commits for uncommitted changes
220
244
  changed_files: changedFiles,
221
245
  source_branch: branchName,
246
+ changed_lines: calculateChangedLines(changedFiles),
222
247
  };
223
248
  } catch (error) {
224
249
  console.error(chalk.red('Failed to analyze uncommitted changes:'), error.message);
@@ -498,6 +523,7 @@ export async function runLocalReview(targetBranch = null, ignorePatterns = null)
498
523
  author_email,
499
524
  author_name,
500
525
  contributors,
526
+ changed_lines: calculateChangedLines(changedFiles),
501
527
  };
502
528
  } catch (error) {
503
529
  console.error(chalk.red('Failed to run local review analysis:'), error.message);
@@ -667,3 +667,166 @@ describe('getContributors', () => {
667
667
  expect(result.contributors[2].commits).toBe(1);
668
668
  });
669
669
  });
670
+
671
+ describe('changed_lines calculation', () => {
672
+ beforeEach(() => {
673
+ vi.mock('execa');
674
+ });
675
+
676
+ afterEach(() => {
677
+ vi.restoreAllMocks();
678
+ });
679
+
680
+ it('should calculate changed_lines for uncommitted changes', async () => {
681
+ vi.mocked(execa).mockImplementation(async (cmd, args) => {
682
+ const command = [cmd, ...args].join(' ');
683
+
684
+ if (command.includes('remote get-url origin')) {
685
+ return { stdout: 'https://github.com/user/repo.git' };
686
+ }
687
+ if (command.includes('rev-parse --abbrev-ref HEAD')) {
688
+ return { stdout: 'feature-branch' };
689
+ }
690
+ if (command.includes('rev-parse --show-toplevel')) {
691
+ return { stdout: '/fake/repo/path' };
692
+ }
693
+ if (command.includes('diff --cached --name-status')) {
694
+ return { stdout: 'M\tfile.js' };
695
+ }
696
+ if (command.includes('diff --cached -U15 -- file.js')) {
697
+ // Simulate a diff with 5 additions and 3 deletions (8 total changed lines)
698
+ return {
699
+ stdout:
700
+ 'diff --git a/file.js b/file.js\n' +
701
+ '--- a/file.js\n' +
702
+ '+++ b/file.js\n' +
703
+ '@@ -1,5 +1,10 @@\n' +
704
+ '+new line 1\n' +
705
+ '+new line 2\n' +
706
+ ' existing line\n' +
707
+ '-old line 1\n' +
708
+ '-old line 2\n' +
709
+ '-old line 3\n' +
710
+ '+new line 3\n' +
711
+ '+new line 4\n' +
712
+ '+new line 5\n',
713
+ };
714
+ }
715
+ if (command.includes('show HEAD:file.js')) {
716
+ return { stdout: 'old content' };
717
+ }
718
+
719
+ throw new Error(`Unmocked command: ${command}`);
720
+ });
721
+
722
+ const result = await runUncommittedReview('staged');
723
+
724
+ expect(result).toBeDefined();
725
+ expect(result.changed_lines).toBe(8); // 5 additions + 3 deletions
726
+ });
727
+
728
+ it('should calculate changed_lines for local review', async () => {
729
+ vi.mocked(execa).mockImplementation(async (cmd, args) => {
730
+ const command = [cmd, ...args].join(' ');
731
+
732
+ if (command.includes('remote get-url origin')) {
733
+ return { stdout: 'https://github.com/user/repo.git' };
734
+ }
735
+ if (command.includes('rev-parse --abbrev-ref HEAD')) {
736
+ return { stdout: 'feature-branch' };
737
+ }
738
+ if (command.includes('rev-parse --show-toplevel')) {
739
+ return { stdout: '/path/to/repo' };
740
+ }
741
+ if (command.includes('rev-parse --verify main')) {
742
+ return { stdout: 'commit-hash' };
743
+ }
744
+ if (command === 'git fetch origin main') {
745
+ return { stdout: '' };
746
+ }
747
+ if (command.includes('merge-base origin/main HEAD')) {
748
+ return { stdout: 'abc123' };
749
+ }
750
+ if (command.includes('log --no-merges --pretty=%B---EOC---')) {
751
+ return { stdout: 'feat: add feature---EOC---' };
752
+ }
753
+ if (command.includes('diff --name-status')) {
754
+ return { stdout: 'M\tfile1.js\nA\tfile2.js' };
755
+ }
756
+ if (command.includes('diff -U15') && command.includes('file1.js')) {
757
+ // 10 lines changed in file1.js
758
+ return {
759
+ stdout:
760
+ 'diff --git a/file1.js b/file1.js\n' +
761
+ '+line1\n' +
762
+ '+line2\n' +
763
+ '-line3\n' +
764
+ '-line4\n' +
765
+ '+line5\n' +
766
+ '+line6\n' +
767
+ '+line7\n' +
768
+ '-line8\n' +
769
+ '+line9\n' +
770
+ '+line10\n',
771
+ };
772
+ }
773
+ if (command.includes('diff -U15') && command.includes('file2.js')) {
774
+ // 5 lines added in file2.js (new file)
775
+ return {
776
+ stdout:
777
+ 'diff --git a/file2.js b/file2.js\n' +
778
+ '+line1\n' +
779
+ '+line2\n' +
780
+ '+line3\n' +
781
+ '+line4\n' +
782
+ '+line5\n',
783
+ };
784
+ }
785
+ if (command.includes('show abc123:file1.js')) {
786
+ return { stdout: 'original content' };
787
+ }
788
+
789
+ return { stdout: '' };
790
+ });
791
+
792
+ const result = await runLocalReview('main');
793
+
794
+ expect(result).toBeDefined();
795
+ expect(result.changed_lines).toBe(15); // 10 from file1.js + 5 from file2.js
796
+ });
797
+
798
+ it('should handle diffs with no changes', async () => {
799
+ vi.mocked(execa).mockImplementation(async (cmd, args) => {
800
+ const command = [cmd, ...args].join(' ');
801
+
802
+ if (command.includes('remote get-url origin')) {
803
+ return { stdout: 'https://github.com/user/repo.git' };
804
+ }
805
+ if (command.includes('rev-parse --abbrev-ref HEAD')) {
806
+ return { stdout: 'feature-branch' };
807
+ }
808
+ if (command.includes('rev-parse --show-toplevel')) {
809
+ return { stdout: '/fake/repo/path' };
810
+ }
811
+ if (command.includes('diff --cached --name-status')) {
812
+ return { stdout: 'M\tfile.js' };
813
+ }
814
+ if (command.includes('diff --cached -U15 -- file.js')) {
815
+ // Empty diff (no actual changes, just headers)
816
+ return {
817
+ stdout: 'diff --git a/file.js b/file.js\n--- a/file.js\n+++ b/file.js\n',
818
+ };
819
+ }
820
+ if (command.includes('show HEAD:file.js')) {
821
+ return { stdout: 'content' };
822
+ }
823
+
824
+ throw new Error(`Unmocked command: ${command}`);
825
+ });
826
+
827
+ const result = await runUncommittedReview('staged');
828
+
829
+ expect(result).toBeDefined();
830
+ expect(result.changed_lines).toBe(0);
831
+ });
832
+ });
package/src/index.js CHANGED
@@ -187,6 +187,7 @@ program
187
187
  )
188
188
  .option('--json', 'Output raw API response as JSON')
189
189
  .option('--comment', 'Post review results as PR comments (auto-detects CI provider)')
190
+ .option('--post-ticket', 'Post review results to linked ticket (e.g., JIRA)')
190
191
  .action(async (targetBranch, options) => {
191
192
  const reviewTarget = targetBranch ? `against '${targetBranch}'` : '(auto-detecting fork point)';
192
193
 
@@ -269,6 +270,11 @@ program
269
270
  spinner.text = `Submitting review to the AI... ${elapsed}s`;
270
271
  }, 1000);
271
272
 
273
+ // Add post_to_ticket flag if requested
274
+ if (options.postTicket) {
275
+ payload.post_to_ticket = true;
276
+ }
277
+
272
278
  try {
273
279
  const response = await axios.post(apiEndpoint, payload, {
274
280
  headers: {