korekt-cli 0.8.3 โ†’ 0.8.5

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.3",
3
+ "version": "0.8.5",
4
4
  "description": "AI-powered code review CLI - Keep your kode korekt",
5
5
  "main": "src/index.js",
6
6
  "bin": {
package/src/formatter.js CHANGED
@@ -29,6 +29,8 @@ const CATEGORY_ICONS = {
29
29
  documentation: '๐Ÿ“',
30
30
  test_coverage: '๐Ÿงช',
31
31
  readability: '๐Ÿ“–',
32
+ architectural: '๐Ÿ—๏ธ',
33
+ file_structure: '๐Ÿ“',
32
34
  default: 'โš™๏ธ', // Default icon
33
35
  };
34
36
 
package/src/git-logic.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { execa } from 'execa';
2
2
  import chalk from 'chalk';
3
+ import { detectCIProvider } from './utils.js';
3
4
 
4
5
  /**
5
6
  * Truncate content to a maximum number of lines using "head and tail".
@@ -98,6 +99,30 @@ export function shouldIgnoreFile(filePath, patterns) {
98
99
  return false;
99
100
  }
100
101
 
102
+ /**
103
+ * Calculate total changed lines (additions + deletions) from changed files
104
+ * @param {Array} changedFiles - Array of file objects with diff property
105
+ * @returns {number} - Total number of changed lines
106
+ */
107
+ export function calculateChangedLines(changedFiles) {
108
+ let changedLines = 0;
109
+ for (const file of changedFiles) {
110
+ if (file.diff) {
111
+ const lines = file.diff.split('\n');
112
+ for (const line of lines) {
113
+ if (
114
+ (line.startsWith('+') || line.startsWith('-')) &&
115
+ !line.startsWith('+++') &&
116
+ !line.startsWith('---')
117
+ ) {
118
+ changedLines++;
119
+ }
120
+ }
121
+ }
122
+ }
123
+ return changedLines;
124
+ }
125
+
101
126
  /**
102
127
  * Helper function to parse the complex output of git diff --name-status
103
128
  */
@@ -219,6 +244,8 @@ export async function runUncommittedReview(mode = 'unstaged') {
219
244
  commit_messages: [], // No commits for uncommitted changes
220
245
  changed_files: changedFiles,
221
246
  source_branch: branchName,
247
+ changed_lines: calculateChangedLines(changedFiles),
248
+ is_ci: detectCIProvider() !== null,
222
249
  };
223
250
  } catch (error) {
224
251
  console.error(chalk.red('Failed to analyze uncommitted changes:'), error.message);
@@ -498,6 +525,8 @@ export async function runLocalReview(targetBranch = null, ignorePatterns = null)
498
525
  author_email,
499
526
  author_name,
500
527
  contributors,
528
+ changed_lines: calculateChangedLines(changedFiles),
529
+ is_ci: detectCIProvider() !== null,
501
530
  };
502
531
  } catch (error) {
503
532
  console.error(chalk.red('Failed to run local review analysis:'), error.message);
@@ -9,6 +9,7 @@ import {
9
9
  getContributors,
10
10
  } from './git-logic.js';
11
11
  import { execa } from 'execa';
12
+ import { detectCIProvider } from './utils.js';
12
13
 
13
14
  describe('parseNameStatus', () => {
14
15
  it('should correctly parse M, A, and D statuses', () => {
@@ -667,3 +668,345 @@ describe('getContributors', () => {
667
668
  expect(result.contributors[2].commits).toBe(1);
668
669
  });
669
670
  });
671
+
672
+ describe('changed_lines calculation', () => {
673
+ beforeEach(() => {
674
+ vi.mock('execa');
675
+ });
676
+
677
+ afterEach(() => {
678
+ vi.restoreAllMocks();
679
+ });
680
+
681
+ it('should calculate changed_lines for uncommitted changes', async () => {
682
+ vi.mocked(execa).mockImplementation(async (cmd, args) => {
683
+ const command = [cmd, ...args].join(' ');
684
+
685
+ if (command.includes('remote get-url origin')) {
686
+ return { stdout: 'https://github.com/user/repo.git' };
687
+ }
688
+ if (command.includes('rev-parse --abbrev-ref HEAD')) {
689
+ return { stdout: 'feature-branch' };
690
+ }
691
+ if (command.includes('rev-parse --show-toplevel')) {
692
+ return { stdout: '/fake/repo/path' };
693
+ }
694
+ if (command.includes('diff --cached --name-status')) {
695
+ return { stdout: 'M\tfile.js' };
696
+ }
697
+ if (command.includes('diff --cached -U15 -- file.js')) {
698
+ // Simulate a diff with 5 additions and 3 deletions (8 total changed lines)
699
+ return {
700
+ stdout:
701
+ 'diff --git a/file.js b/file.js\n' +
702
+ '--- a/file.js\n' +
703
+ '+++ b/file.js\n' +
704
+ '@@ -1,5 +1,10 @@\n' +
705
+ '+new line 1\n' +
706
+ '+new line 2\n' +
707
+ ' existing line\n' +
708
+ '-old line 1\n' +
709
+ '-old line 2\n' +
710
+ '-old line 3\n' +
711
+ '+new line 3\n' +
712
+ '+new line 4\n' +
713
+ '+new line 5\n',
714
+ };
715
+ }
716
+ if (command.includes('show HEAD:file.js')) {
717
+ return { stdout: 'old content' };
718
+ }
719
+
720
+ throw new Error(`Unmocked command: ${command}`);
721
+ });
722
+
723
+ const result = await runUncommittedReview('staged');
724
+
725
+ expect(result).toBeDefined();
726
+ expect(result.changed_lines).toBe(8); // 5 additions + 3 deletions
727
+ });
728
+
729
+ it('should calculate changed_lines for local review', async () => {
730
+ vi.mocked(execa).mockImplementation(async (cmd, args) => {
731
+ const command = [cmd, ...args].join(' ');
732
+
733
+ if (command.includes('remote get-url origin')) {
734
+ return { stdout: 'https://github.com/user/repo.git' };
735
+ }
736
+ if (command.includes('rev-parse --abbrev-ref HEAD')) {
737
+ return { stdout: 'feature-branch' };
738
+ }
739
+ if (command.includes('rev-parse --show-toplevel')) {
740
+ return { stdout: '/path/to/repo' };
741
+ }
742
+ if (command.includes('rev-parse --verify main')) {
743
+ return { stdout: 'commit-hash' };
744
+ }
745
+ if (command === 'git fetch origin main') {
746
+ return { stdout: '' };
747
+ }
748
+ if (command.includes('merge-base origin/main HEAD')) {
749
+ return { stdout: 'abc123' };
750
+ }
751
+ if (command.includes('log --no-merges --pretty=%B---EOC---')) {
752
+ return { stdout: 'feat: add feature---EOC---' };
753
+ }
754
+ if (command.includes('diff --name-status')) {
755
+ return { stdout: 'M\tfile1.js\nA\tfile2.js' };
756
+ }
757
+ if (command.includes('diff -U15') && command.includes('file1.js')) {
758
+ // 10 lines changed in file1.js
759
+ return {
760
+ stdout:
761
+ 'diff --git a/file1.js b/file1.js\n' +
762
+ '+line1\n' +
763
+ '+line2\n' +
764
+ '-line3\n' +
765
+ '-line4\n' +
766
+ '+line5\n' +
767
+ '+line6\n' +
768
+ '+line7\n' +
769
+ '-line8\n' +
770
+ '+line9\n' +
771
+ '+line10\n',
772
+ };
773
+ }
774
+ if (command.includes('diff -U15') && command.includes('file2.js')) {
775
+ // 5 lines added in file2.js (new file)
776
+ return {
777
+ stdout:
778
+ 'diff --git a/file2.js b/file2.js\n' +
779
+ '+line1\n' +
780
+ '+line2\n' +
781
+ '+line3\n' +
782
+ '+line4\n' +
783
+ '+line5\n',
784
+ };
785
+ }
786
+ if (command.includes('show abc123:file1.js')) {
787
+ return { stdout: 'original content' };
788
+ }
789
+
790
+ return { stdout: '' };
791
+ });
792
+
793
+ const result = await runLocalReview('main');
794
+
795
+ expect(result).toBeDefined();
796
+ expect(result.changed_lines).toBe(15); // 10 from file1.js + 5 from file2.js
797
+ });
798
+
799
+ it('should handle diffs with no changes', async () => {
800
+ vi.mocked(execa).mockImplementation(async (cmd, args) => {
801
+ const command = [cmd, ...args].join(' ');
802
+
803
+ if (command.includes('remote get-url origin')) {
804
+ return { stdout: 'https://github.com/user/repo.git' };
805
+ }
806
+ if (command.includes('rev-parse --abbrev-ref HEAD')) {
807
+ return { stdout: 'feature-branch' };
808
+ }
809
+ if (command.includes('rev-parse --show-toplevel')) {
810
+ return { stdout: '/fake/repo/path' };
811
+ }
812
+ if (command.includes('diff --cached --name-status')) {
813
+ return { stdout: 'M\tfile.js' };
814
+ }
815
+ if (command.includes('diff --cached -U15 -- file.js')) {
816
+ // Empty diff (no actual changes, just headers)
817
+ return {
818
+ stdout: 'diff --git a/file.js b/file.js\n--- a/file.js\n+++ b/file.js\n',
819
+ };
820
+ }
821
+ if (command.includes('show HEAD:file.js')) {
822
+ return { stdout: 'content' };
823
+ }
824
+
825
+ throw new Error(`Unmocked command: ${command}`);
826
+ });
827
+
828
+ const result = await runUncommittedReview('staged');
829
+
830
+ expect(result).toBeDefined();
831
+ expect(result.changed_lines).toBe(0);
832
+ });
833
+ });
834
+
835
+ describe('is_ci flag in payload', () => {
836
+ beforeEach(() => {
837
+ vi.mock('execa');
838
+ vi.mock('./utils.js', () => ({
839
+ detectCIProvider: vi.fn(),
840
+ }));
841
+ });
842
+
843
+ afterEach(() => {
844
+ vi.restoreAllMocks();
845
+ });
846
+
847
+ it('should set is_ci to true when detectCIProvider returns a provider', async () => {
848
+ vi.mocked(detectCIProvider).mockReturnValue('github');
849
+
850
+ vi.mocked(execa).mockImplementation(async (cmd, args) => {
851
+ const command = [cmd, ...args].join(' ');
852
+
853
+ if (command.includes('remote get-url origin')) {
854
+ return { stdout: 'https://github.com/user/repo.git' };
855
+ }
856
+ if (command.includes('rev-parse --abbrev-ref HEAD')) {
857
+ return { stdout: 'feature-branch' };
858
+ }
859
+ if (command.includes('rev-parse --show-toplevel')) {
860
+ return { stdout: '/fake/repo/path' };
861
+ }
862
+ if (command.includes('diff --cached --name-status')) {
863
+ return { stdout: 'M\tfile.js' };
864
+ }
865
+ if (command.includes('diff --cached -U15 -- file.js')) {
866
+ return { stdout: 'diff --git a/file.js b/file.js\n+new line' };
867
+ }
868
+ if (command.includes('show HEAD:file.js')) {
869
+ return { stdout: 'old content' };
870
+ }
871
+
872
+ throw new Error(`Unmocked command: ${command}`);
873
+ });
874
+
875
+ const result = await runUncommittedReview('staged');
876
+
877
+ expect(result).toBeDefined();
878
+ expect(result.is_ci).toBe(true);
879
+ });
880
+
881
+ it('should set is_ci to false when detectCIProvider returns null', async () => {
882
+ vi.mocked(detectCIProvider).mockReturnValue(null);
883
+
884
+ vi.mocked(execa).mockImplementation(async (cmd, args) => {
885
+ const command = [cmd, ...args].join(' ');
886
+
887
+ if (command.includes('remote get-url origin')) {
888
+ return { stdout: 'https://github.com/user/repo.git' };
889
+ }
890
+ if (command.includes('rev-parse --abbrev-ref HEAD')) {
891
+ return { stdout: 'feature-branch' };
892
+ }
893
+ if (command.includes('rev-parse --show-toplevel')) {
894
+ return { stdout: '/fake/repo/path' };
895
+ }
896
+ if (command.includes('diff --cached --name-status')) {
897
+ return { stdout: 'M\tfile.js' };
898
+ }
899
+ if (command.includes('diff --cached -U15 -- file.js')) {
900
+ return { stdout: 'diff --git a/file.js b/file.js\n+new line' };
901
+ }
902
+ if (command.includes('show HEAD:file.js')) {
903
+ return { stdout: 'old content' };
904
+ }
905
+
906
+ throw new Error(`Unmocked command: ${command}`);
907
+ });
908
+
909
+ const result = await runUncommittedReview('staged');
910
+
911
+ expect(result).toBeDefined();
912
+ expect(result.is_ci).toBe(false);
913
+ });
914
+
915
+ it('should include is_ci in runLocalReview payload', async () => {
916
+ vi.mocked(detectCIProvider).mockReturnValue('azure');
917
+
918
+ vi.mocked(execa).mockImplementation(async (cmd, args) => {
919
+ const command = [cmd, ...args].join(' ');
920
+
921
+ if (command.includes('remote get-url origin')) {
922
+ return { stdout: 'https://github.com/user/repo.git' };
923
+ }
924
+ if (command.includes('rev-parse --abbrev-ref HEAD')) {
925
+ return { stdout: 'feature-branch' };
926
+ }
927
+ if (command.includes('rev-parse --show-toplevel')) {
928
+ return { stdout: '/path/to/repo' };
929
+ }
930
+ if (command.includes('rev-parse --verify main')) {
931
+ return { stdout: 'commit-hash' };
932
+ }
933
+ if (command === 'git fetch origin main') {
934
+ return { stdout: '' };
935
+ }
936
+ if (command.includes('merge-base origin/main HEAD')) {
937
+ return { stdout: 'abc123' };
938
+ }
939
+ if (command.includes('log --no-merges --pretty=%B---EOC---')) {
940
+ return { stdout: 'feat: add feature---EOC---' };
941
+ }
942
+ if (command.includes('log --no-merges --format=%ae|%an')) {
943
+ return { stdout: 'user@example.com|User Name' };
944
+ }
945
+ if (command.includes('diff --name-status')) {
946
+ return { stdout: 'M\tfile.js' };
947
+ }
948
+ if (command.includes('diff -U15')) {
949
+ return { stdout: 'diff content' };
950
+ }
951
+ if (command.includes('show abc123:file.js')) {
952
+ return { stdout: 'original content' };
953
+ }
954
+
955
+ return { stdout: '' };
956
+ });
957
+
958
+ const result = await runLocalReview('main');
959
+
960
+ expect(result).toBeDefined();
961
+ expect(result.is_ci).toBe(true);
962
+ });
963
+
964
+ it('should set is_ci to false in runLocalReview when not in CI', async () => {
965
+ vi.mocked(detectCIProvider).mockReturnValue(null);
966
+
967
+ vi.mocked(execa).mockImplementation(async (cmd, args) => {
968
+ const command = [cmd, ...args].join(' ');
969
+
970
+ if (command.includes('remote get-url origin')) {
971
+ return { stdout: 'https://github.com/user/repo.git' };
972
+ }
973
+ if (command.includes('rev-parse --abbrev-ref HEAD')) {
974
+ return { stdout: 'feature-branch' };
975
+ }
976
+ if (command.includes('rev-parse --show-toplevel')) {
977
+ return { stdout: '/path/to/repo' };
978
+ }
979
+ if (command.includes('rev-parse --verify main')) {
980
+ return { stdout: 'commit-hash' };
981
+ }
982
+ if (command === 'git fetch origin main') {
983
+ return { stdout: '' };
984
+ }
985
+ if (command.includes('merge-base origin/main HEAD')) {
986
+ return { stdout: 'abc123' };
987
+ }
988
+ if (command.includes('log --no-merges --pretty=%B---EOC---')) {
989
+ return { stdout: 'feat: add feature---EOC---' };
990
+ }
991
+ if (command.includes('log --no-merges --format=%ae|%an')) {
992
+ return { stdout: 'user@example.com|User Name' };
993
+ }
994
+ if (command.includes('diff --name-status')) {
995
+ return { stdout: 'M\tfile.js' };
996
+ }
997
+ if (command.includes('diff -U15')) {
998
+ return { stdout: 'diff content' };
999
+ }
1000
+ if (command.includes('show abc123:file.js')) {
1001
+ return { stdout: 'original content' };
1002
+ }
1003
+
1004
+ return { stdout: '' };
1005
+ });
1006
+
1007
+ const result = await runLocalReview('main');
1008
+
1009
+ expect(result).toBeDefined();
1010
+ expect(result.is_ci).toBe(false);
1011
+ });
1012
+ });
package/src/index.js CHANGED
@@ -14,6 +14,10 @@ import { tmpdir } from 'os';
14
14
  import { runLocalReview } from './git-logic.js';
15
15
  import { getApiKey, setApiKey, getApiEndpoint, setApiEndpoint } from './config.js';
16
16
  import { formatReviewOutput } from './formatter.js';
17
+ import { detectCIProvider, truncateFileData, formatErrorOutput } from './utils.js';
18
+
19
+ // Re-export utilities for backward compatibility
20
+ export { detectCIProvider, truncateFileData, formatErrorOutput } from './utils.js';
17
21
 
18
22
  const require = createRequire(import.meta.url);
19
23
  const { version } = require('../package.json');
@@ -26,44 +30,6 @@ const { version } = require('../package.json');
26
30
  const log = (msg) => process.stderr.write(msg + '\n');
27
31
  const output = (msg) => process.stdout.write(msg + '\n');
28
32
 
29
- /**
30
- * Truncates file data (diff and content) for display purposes
31
- * @param {Object} file - File object with path, status, diff, content, etc.
32
- * @param {number} maxLength - Maximum length before truncation (default: 500)
33
- * @returns {Object} File object with truncated diff and content
34
- */
35
- export function truncateFileData(file, maxLength = 500) {
36
- return {
37
- path: file.path,
38
- status: file.status,
39
- ...(file.old_path && { old_path: file.old_path }),
40
- diff:
41
- file.diff.length > maxLength
42
- ? `${file.diff.substring(0, maxLength)}... [truncated ${file.diff.length - maxLength} chars]`
43
- : file.diff,
44
- content:
45
- file.content.length > maxLength
46
- ? `${file.content.substring(0, maxLength)}... [truncated ${file.content.length - maxLength} chars]`
47
- : file.content,
48
- };
49
- }
50
-
51
- /**
52
- * Formats error object for JSON output
53
- * @param {Error} error - Error object from axios or other source
54
- * @returns {Object} Formatted error output with success: false
55
- */
56
- export function formatErrorOutput(error) {
57
- return {
58
- success: false,
59
- error: error.message,
60
- ...(error.response && {
61
- status: error.response.status,
62
- data: error.response.data,
63
- }),
64
- };
65
- }
66
-
67
33
  /**
68
34
  * Ask for user confirmation before proceeding
69
35
  */
@@ -84,23 +50,6 @@ async function confirmAction(message) {
84
50
  });
85
51
  }
86
52
 
87
- /**
88
- * Detect CI provider from environment variables
89
- * @returns {string|null} Provider name or null if not detected
90
- */
91
- export function detectCIProvider() {
92
- if (process.env.GITHUB_TOKEN && process.env.GITHUB_REPOSITORY) {
93
- return 'github';
94
- }
95
- if (process.env.SYSTEM_ACCESSTOKEN && process.env.SYSTEM_PULLREQUEST_PULLREQUESTID) {
96
- return 'azure';
97
- }
98
- if (process.env.BITBUCKET_REPO_SLUG && process.env.BITBUCKET_PR_ID) {
99
- return 'bitbucket';
100
- }
101
- return null;
102
- }
103
-
104
53
  /**
105
54
  * Run the CI integration script to post comments
106
55
  * @param {string} provider - CI provider (github, azure, bitbucket)
package/src/utils.js ADDED
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Detect CI provider from environment variables
3
+ * @returns {string|null} Provider name or null if not detected
4
+ */
5
+ export function detectCIProvider() {
6
+ if (process.env.GITHUB_TOKEN && process.env.GITHUB_REPOSITORY) {
7
+ return 'github';
8
+ }
9
+ if (process.env.SYSTEM_ACCESSTOKEN && process.env.SYSTEM_PULLREQUEST_PULLREQUESTID) {
10
+ return 'azure';
11
+ }
12
+ if (process.env.BITBUCKET_REPO_SLUG && process.env.BITBUCKET_PR_ID) {
13
+ return 'bitbucket';
14
+ }
15
+ return null;
16
+ }
17
+
18
+ /**
19
+ * Truncates file data (diff and content) for display purposes
20
+ * @param {Object} file - File object with path, status, diff, content, etc.
21
+ * @param {number} maxLength - Maximum length before truncation (default: 500)
22
+ * @returns {Object} File object with truncated diff and content
23
+ */
24
+ export function truncateFileData(file, maxLength = 500) {
25
+ return {
26
+ path: file.path,
27
+ status: file.status,
28
+ ...(file.old_path && { old_path: file.old_path }),
29
+ diff:
30
+ file.diff.length > maxLength
31
+ ? `${file.diff.substring(0, maxLength)}... [truncated ${file.diff.length - maxLength} chars]`
32
+ : file.diff,
33
+ content:
34
+ file.content.length > maxLength
35
+ ? `${file.content.substring(0, maxLength)}... [truncated ${file.content.length - maxLength} chars]`
36
+ : file.content,
37
+ };
38
+ }
39
+
40
+ /**
41
+ * Formats error object for JSON output
42
+ * @param {Error} error - Error object from axios or other source
43
+ * @returns {Object} Formatted error output with success: false
44
+ */
45
+ export function formatErrorOutput(error) {
46
+ return {
47
+ success: false,
48
+ error: error.message,
49
+ ...(error.response && {
50
+ status: error.response.status,
51
+ data: error.response.data,
52
+ }),
53
+ };
54
+ }