korekt-cli 0.5.0 ā 0.7.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/README.md +5 -20
- package/package.json +1 -1
- package/src/config.js +1 -20
- package/src/git-logic.js +66 -64
- package/src/git-logic.test.js +141 -48
- package/src/index.js +9 -88
package/README.md
CHANGED
|
@@ -12,7 +12,7 @@ AI-powered code review CLI - Keep your kode korekt
|
|
|
12
12
|
|
|
13
13
|
* **AI-Powered Analysis**: Get instant, intelligent code reviews with severity levels, categories, and actionable suggestions
|
|
14
14
|
* **Local Git Integration**: Works with committed changes, staged changes, and unstaged modifications
|
|
15
|
-
* **Ticket
|
|
15
|
+
* **Ticket Context Enrichment**: Server-side ticket extraction from branch names and commit messages (Jira & Azure DevOps)
|
|
16
16
|
* **Beautiful Output**: Color-coded issues with severity indicators, file locations, and suggested fixes
|
|
17
17
|
* **Ultra-Fast**: Short command syntax (`kk`) for maximum developer efficiency
|
|
18
18
|
|
|
@@ -28,7 +28,7 @@ Configure the CLI with your API credentials:
|
|
|
28
28
|
|
|
29
29
|
```bash
|
|
30
30
|
kk config --key YOUR_API_KEY
|
|
31
|
-
kk config --endpoint https://api.korekt.ai/review
|
|
31
|
+
kk config --endpoint https://api.korekt.ai/api/review
|
|
32
32
|
```
|
|
33
33
|
|
|
34
34
|
Run your first review:
|
|
@@ -43,8 +43,7 @@ kk stg
|
|
|
43
43
|
# Review only unstaged changes
|
|
44
44
|
kk diff
|
|
45
45
|
|
|
46
|
-
|
|
47
|
-
kk all
|
|
46
|
+
|
|
48
47
|
```
|
|
49
48
|
|
|
50
49
|
## Usage
|
|
@@ -56,10 +55,7 @@ kk all
|
|
|
56
55
|
kk config --key YOUR_API_KEY
|
|
57
56
|
|
|
58
57
|
# Set API endpoint
|
|
59
|
-
kk config --endpoint https://api.korekt.ai/review
|
|
60
|
-
|
|
61
|
-
# Set default ticket system (jira or ado)
|
|
62
|
-
kk config --ticket-system jira
|
|
58
|
+
kk config --endpoint https://api.korekt.ai/api/review
|
|
63
59
|
|
|
64
60
|
# Show current configuration
|
|
65
61
|
kk config --show
|
|
@@ -74,9 +70,6 @@ kk review
|
|
|
74
70
|
# Review against specific branch
|
|
75
71
|
kk review main
|
|
76
72
|
|
|
77
|
-
# Review with ticket system override
|
|
78
|
-
kk review main --ticket-system ado
|
|
79
|
-
|
|
80
73
|
# Review with ignored files
|
|
81
74
|
kk review main --ignore "*.lock" "dist/*"
|
|
82
75
|
|
|
@@ -93,16 +86,9 @@ kk stg
|
|
|
93
86
|
# Review unstaged changes only
|
|
94
87
|
kk diff
|
|
95
88
|
|
|
96
|
-
# Review all uncommitted changes
|
|
97
|
-
kk all
|
|
98
|
-
|
|
99
|
-
# Include untracked files
|
|
100
|
-
kk all --untracked
|
|
101
|
-
|
|
102
89
|
# JSON output works with all review commands
|
|
103
90
|
kk stg --json
|
|
104
91
|
kk diff --json
|
|
105
|
-
kk all --json
|
|
106
92
|
```
|
|
107
93
|
|
|
108
94
|
### Alternative Command
|
|
@@ -119,8 +105,7 @@ You can also configure using environment variables:
|
|
|
119
105
|
|
|
120
106
|
```bash
|
|
121
107
|
export KOREKT_API_KEY="your-api-key"
|
|
122
|
-
export KOREKT_API_ENDPOINT="https://api.korekt.ai/review
|
|
123
|
-
export KOREKT_TICKET_SYSTEM="jira"
|
|
108
|
+
export KOREKT_API_ENDPOINT="https://api.korekt.ai/api/review"
|
|
124
109
|
```
|
|
125
110
|
|
|
126
111
|
Note: Config file takes precedence over environment variables.
|
package/package.json
CHANGED
package/src/config.js
CHANGED
|
@@ -35,7 +35,7 @@ export function getApiEndpoint() {
|
|
|
35
35
|
const configEndpoint = config.get('apiEndpoint');
|
|
36
36
|
if (configEndpoint) return configEndpoint;
|
|
37
37
|
|
|
38
|
-
return process.env.KOREKT_API_ENDPOINT || 'https://api.korekt.ai/api/review
|
|
38
|
+
return process.env.KOREKT_API_ENDPOINT || 'https://api.korekt.ai/api/review';
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
/**
|
|
@@ -45,24 +45,6 @@ export function setApiEndpoint(endpoint) {
|
|
|
45
45
|
config.set('apiEndpoint', endpoint);
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
/**
|
|
49
|
-
* Get the ticket system from config or environment
|
|
50
|
-
* Priority: 1) config store, 2) .env file
|
|
51
|
-
*/
|
|
52
|
-
export function getTicketSystem() {
|
|
53
|
-
const configTicketSystem = config.get('ticketSystem');
|
|
54
|
-
if (configTicketSystem) return configTicketSystem;
|
|
55
|
-
|
|
56
|
-
return process.env.KOREKT_TICKET_SYSTEM || null;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* Set the ticket system in config store
|
|
61
|
-
*/
|
|
62
|
-
export function setTicketSystem(system) {
|
|
63
|
-
config.set('ticketSystem', system);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
48
|
/**
|
|
67
49
|
* Get all configuration
|
|
68
50
|
*/
|
|
@@ -70,6 +52,5 @@ export function getConfig() {
|
|
|
70
52
|
return {
|
|
71
53
|
apiKey: getApiKey(),
|
|
72
54
|
apiEndpoint: getApiEndpoint(),
|
|
73
|
-
ticketSystem: getTicketSystem(),
|
|
74
55
|
};
|
|
75
56
|
}
|
package/src/git-logic.js
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
import { execa } from 'execa';
|
|
2
2
|
import chalk from 'chalk';
|
|
3
|
-
import fs from 'fs';
|
|
4
|
-
import path from 'path';
|
|
5
3
|
|
|
6
4
|
/**
|
|
7
5
|
* Truncate content to a maximum number of lines using "head and tail".
|
|
@@ -136,16 +134,11 @@ export function parseNameStatus(output) {
|
|
|
136
134
|
}
|
|
137
135
|
|
|
138
136
|
/**
|
|
139
|
-
* Analyze uncommitted changes (staged
|
|
140
|
-
* @param {string} mode - 'staged'
|
|
141
|
-
* @param {string|null} ticketSystem - The ticket system to use (jira or ado), or null to skip ticket extraction
|
|
137
|
+
* Analyze uncommitted changes (staged or unstaged)
|
|
138
|
+
* @param {string} mode - 'staged' or 'unstaged'
|
|
142
139
|
* @returns {Object|null} - The payload object ready for API submission, or null on error
|
|
143
140
|
*/
|
|
144
|
-
export async function runUncommittedReview(
|
|
145
|
-
mode = 'all',
|
|
146
|
-
_ticketSystem = null,
|
|
147
|
-
includeUntracked = false
|
|
148
|
-
) {
|
|
141
|
+
export async function runUncommittedReview(mode = 'unstaged') {
|
|
149
142
|
try {
|
|
150
143
|
// 1. Get Repo URL, current branch name, and repository root
|
|
151
144
|
const { stdout: repoUrl } = await execa('git', ['remote', 'get-url', 'origin']);
|
|
@@ -167,67 +160,23 @@ export async function runUncommittedReview(
|
|
|
167
160
|
if (mode === 'staged') {
|
|
168
161
|
nameStatusOutput = await git('diff', '--cached', '--name-status');
|
|
169
162
|
console.error(chalk.gray('Analyzing staged changes...'));
|
|
170
|
-
} else
|
|
163
|
+
} else {
|
|
171
164
|
nameStatusOutput = await git('diff', '--name-status');
|
|
172
165
|
console.error(chalk.gray('Analyzing unstaged changes...'));
|
|
173
|
-
} else {
|
|
174
|
-
// mode === 'all': combine staged and unstaged
|
|
175
|
-
const staged = await git('diff', '--cached', '--name-status');
|
|
176
|
-
const unstaged = await git('diff', '--name-status');
|
|
177
|
-
nameStatusOutput = [staged, unstaged].filter(Boolean).join('\n');
|
|
178
|
-
console.error(chalk.gray('Analyzing all uncommitted changes...'));
|
|
179
166
|
}
|
|
180
167
|
|
|
181
168
|
const fileList = parseNameStatus(nameStatusOutput);
|
|
182
169
|
const changedFiles = [];
|
|
183
170
|
|
|
184
|
-
|
|
185
|
-
if (includeUntracked) {
|
|
186
|
-
console.error(chalk.gray('Analyzing untracked files...'));
|
|
187
|
-
const untrackedFilesOutput = await git('ls-files', '--others', '--exclude-standard');
|
|
188
|
-
const untrackedFiles = untrackedFilesOutput.split('\n').filter(Boolean);
|
|
189
|
-
|
|
190
|
-
for (const file of untrackedFiles) {
|
|
191
|
-
const fullPath = path.join(repoRootPath, file);
|
|
192
|
-
const content = fs.readFileSync(fullPath, 'utf-8');
|
|
193
|
-
const diff = content
|
|
194
|
-
.split('\n')
|
|
195
|
-
.map((line) => `+${line}`)
|
|
196
|
-
.join('\n');
|
|
197
|
-
changedFiles.push({
|
|
198
|
-
path: file,
|
|
199
|
-
status: 'A', // Untracked files are always additions
|
|
200
|
-
diff: diff,
|
|
201
|
-
content: '', // No old content
|
|
202
|
-
});
|
|
203
|
-
// Add to fileList to prevent duplication if it's also in nameStatusOutput (edge case)
|
|
204
|
-
fileList.push({ status: 'A', path: file, oldPath: file });
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
// Deduplicate file list before processing diffs
|
|
209
|
-
const processedPaths = new Set(changedFiles.map((f) => f.path));
|
|
210
|
-
const uniqueFileList = fileList.filter((file) => !processedPaths.has(file.path));
|
|
211
|
-
|
|
212
|
-
for (const file of uniqueFileList) {
|
|
171
|
+
for (const file of fileList) {
|
|
213
172
|
const { status, path, oldPath } = file;
|
|
214
173
|
|
|
215
174
|
// Get diff for this file
|
|
216
175
|
let diff;
|
|
217
176
|
if (mode === 'staged') {
|
|
218
177
|
diff = await git('diff', '--cached', '-U15', '--', path);
|
|
219
|
-
} else if (mode === 'unstaged') {
|
|
220
|
-
diff = await git('diff', '-U15', '--', path);
|
|
221
178
|
} else {
|
|
222
|
-
|
|
223
|
-
try {
|
|
224
|
-
diff = await git('diff', '--cached', '-U15', '--', path);
|
|
225
|
-
if (!diff) {
|
|
226
|
-
diff = await git('diff', '-U15', '--', path);
|
|
227
|
-
}
|
|
228
|
-
} catch {
|
|
229
|
-
diff = await git('diff', '-U15', '--', path);
|
|
230
|
-
}
|
|
179
|
+
diff = await git('diff', '-U15', '--', path);
|
|
231
180
|
}
|
|
232
181
|
|
|
233
182
|
// Get current content from HEAD (before changes)
|
|
@@ -280,18 +229,62 @@ export async function runUncommittedReview(
|
|
|
280
229
|
}
|
|
281
230
|
}
|
|
282
231
|
|
|
232
|
+
/**
|
|
233
|
+
* Extract contributors from git commits in a range
|
|
234
|
+
* Returns the author (most commits) and full list of contributors
|
|
235
|
+
* @param {string} diffRange - The git range to analyze (e.g., "abc123..HEAD")
|
|
236
|
+
* @param {string} repoRootPath - The repository root directory
|
|
237
|
+
* @returns {Object} - { author_email, author_name, contributors[] }
|
|
238
|
+
*/
|
|
239
|
+
export async function getContributors(diffRange, repoRootPath) {
|
|
240
|
+
try {
|
|
241
|
+
// Get all commit authors with email and name
|
|
242
|
+
const { stdout: authorOutput } = await execa('git', ['log', '--format=%ae|%an', diffRange], {
|
|
243
|
+
cwd: repoRootPath,
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
if (!authorOutput.trim()) {
|
|
247
|
+
return { author_email: null, author_name: null, contributors: [] };
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const lines = authorOutput.trim().split('\n').filter(Boolean);
|
|
251
|
+
|
|
252
|
+
// Count commits per email and track name
|
|
253
|
+
const contributorMap = new Map();
|
|
254
|
+
for (const line of lines) {
|
|
255
|
+
const [email, name] = line.split('|');
|
|
256
|
+
if (!email) continue;
|
|
257
|
+
|
|
258
|
+
if (!contributorMap.has(email)) {
|
|
259
|
+
contributorMap.set(email, { email, name: name || email, commits: 0 });
|
|
260
|
+
}
|
|
261
|
+
contributorMap.get(email).commits++;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Convert to array and sort by commits (descending)
|
|
265
|
+
const contributors = Array.from(contributorMap.values()).sort((a, b) => b.commits - a.commits);
|
|
266
|
+
|
|
267
|
+
// Author = most commits
|
|
268
|
+
const author = contributors[0] || null;
|
|
269
|
+
|
|
270
|
+
return {
|
|
271
|
+
author_email: author?.email || null,
|
|
272
|
+
author_name: author?.name || null,
|
|
273
|
+
contributors,
|
|
274
|
+
};
|
|
275
|
+
} catch (error) {
|
|
276
|
+
console.warn(chalk.yellow('Could not extract contributors:'), error.message);
|
|
277
|
+
return { author_email: null, author_name: null, contributors: [] };
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
283
281
|
/**
|
|
284
282
|
* Main function to analyze local git changes and prepare review payload
|
|
285
283
|
* @param {string|null} targetBranch - The branch to compare against. If null, uses git reflog to find fork point.
|
|
286
|
-
* @param {string|null} ticketSystem - The ticket system to use (jira or ado), or null to skip ticket extraction
|
|
287
284
|
* @param {string[]|null} ignorePatterns - Array of glob patterns to ignore files
|
|
288
285
|
* @returns {Object|null} - The payload object ready for API submission, or null on error
|
|
289
286
|
*/
|
|
290
|
-
export async function runLocalReview(
|
|
291
|
-
targetBranch = null,
|
|
292
|
-
_ticketSystem = null,
|
|
293
|
-
ignorePatterns = null
|
|
294
|
-
) {
|
|
287
|
+
export async function runLocalReview(targetBranch = null, ignorePatterns = null) {
|
|
295
288
|
try {
|
|
296
289
|
// 1. Get Repo URL, current branch name, and repository root
|
|
297
290
|
const { stdout: repoUrl } = await execa('git', ['remote', 'get-url', 'origin']);
|
|
@@ -486,12 +479,21 @@ export async function runLocalReview(
|
|
|
486
479
|
});
|
|
487
480
|
}
|
|
488
481
|
|
|
489
|
-
// 5.
|
|
482
|
+
// 5. Get contributors from commits
|
|
483
|
+
const { author_email, author_name, contributors } = await getContributors(
|
|
484
|
+
diffRange,
|
|
485
|
+
repoRootPath
|
|
486
|
+
);
|
|
487
|
+
|
|
488
|
+
// 6. Assemble the final payload
|
|
490
489
|
return {
|
|
491
490
|
repo_url: normalizeRepoUrl(repoUrl.trim()),
|
|
492
491
|
commit_messages: commitMessages,
|
|
493
492
|
changed_files: changedFiles,
|
|
494
493
|
source_branch: branchName,
|
|
494
|
+
author_email,
|
|
495
|
+
author_name,
|
|
496
|
+
contributors,
|
|
495
497
|
};
|
|
496
498
|
} catch (error) {
|
|
497
499
|
console.error(chalk.red('Failed to run local review analysis:'), error.message);
|
package/src/git-logic.test.js
CHANGED
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
truncateContent,
|
|
7
7
|
normalizeRepoUrl,
|
|
8
8
|
shouldIgnoreFile,
|
|
9
|
+
getContributors,
|
|
9
10
|
} from './git-logic.js';
|
|
10
11
|
import { execa } from 'execa';
|
|
11
12
|
|
|
@@ -82,7 +83,7 @@ describe('runUncommittedReview', () => {
|
|
|
82
83
|
throw new Error(`Unmocked command: ${command}`);
|
|
83
84
|
});
|
|
84
85
|
|
|
85
|
-
const result = await runUncommittedReview('staged'
|
|
86
|
+
const result = await runUncommittedReview('staged');
|
|
86
87
|
|
|
87
88
|
expect(result).toBeDefined();
|
|
88
89
|
expect(result.repo_url).toBe('https://github.com/user/repo'); // Normalized (no .git)
|
|
@@ -119,56 +120,13 @@ describe('runUncommittedReview', () => {
|
|
|
119
120
|
throw new Error(`Unmocked command: ${command}`);
|
|
120
121
|
});
|
|
121
122
|
|
|
122
|
-
const result = await runUncommittedReview('unstaged'
|
|
123
|
+
const result = await runUncommittedReview('unstaged');
|
|
123
124
|
|
|
124
125
|
expect(result).toBeDefined();
|
|
125
126
|
expect(result.source_branch).toBe('feature-branch');
|
|
126
127
|
expect(result.changed_files).toHaveLength(1);
|
|
127
128
|
});
|
|
128
129
|
|
|
129
|
-
it('should analyze all uncommitted changes', async () => {
|
|
130
|
-
vi.mocked(execa).mockImplementation(async (cmd, args) => {
|
|
131
|
-
const command = [cmd, ...args].join(' ');
|
|
132
|
-
|
|
133
|
-
if (command.includes('remote get-url origin')) {
|
|
134
|
-
return { stdout: 'https://github.com/user/repo.git' };
|
|
135
|
-
}
|
|
136
|
-
if (command.includes('rev-parse --abbrev-ref HEAD')) {
|
|
137
|
-
return { stdout: 'feature-branch' };
|
|
138
|
-
}
|
|
139
|
-
if (command.includes('rev-parse --show-toplevel')) {
|
|
140
|
-
return { stdout: '/fake/repo/path' };
|
|
141
|
-
}
|
|
142
|
-
if (command.includes('diff --cached --name-status')) {
|
|
143
|
-
return { stdout: 'M\tstaged.js' };
|
|
144
|
-
}
|
|
145
|
-
if (command === 'git diff --name-status') {
|
|
146
|
-
return { stdout: 'M\tunstaged.js' };
|
|
147
|
-
}
|
|
148
|
-
if (command.includes('diff --cached -U15 -- staged.js')) {
|
|
149
|
-
return { stdout: 'diff staged' };
|
|
150
|
-
}
|
|
151
|
-
if (command.includes('diff -U15 -- unstaged.js')) {
|
|
152
|
-
return { stdout: 'diff unstaged' };
|
|
153
|
-
}
|
|
154
|
-
if (command.includes('show HEAD:staged.js')) {
|
|
155
|
-
return { stdout: 'staged old content' };
|
|
156
|
-
}
|
|
157
|
-
if (command.includes('show HEAD:unstaged.js')) {
|
|
158
|
-
return { stdout: 'unstaged old content' };
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
throw new Error(`Unmocked command: ${command}`);
|
|
162
|
-
});
|
|
163
|
-
|
|
164
|
-
const result = await runUncommittedReview('all', null);
|
|
165
|
-
|
|
166
|
-
expect(result).toBeDefined();
|
|
167
|
-
expect(result.changed_files).toHaveLength(2);
|
|
168
|
-
expect(result.changed_files[0].path).toBe('staged.js');
|
|
169
|
-
expect(result.changed_files[1].path).toBe('unstaged.js');
|
|
170
|
-
});
|
|
171
|
-
|
|
172
130
|
it('should return null when no changes found', async () => {
|
|
173
131
|
vi.mocked(execa).mockImplementation(async (cmd, args) => {
|
|
174
132
|
const command = [cmd, ...args].join(' ');
|
|
@@ -192,7 +150,7 @@ describe('runUncommittedReview', () => {
|
|
|
192
150
|
throw new Error(`Unmocked command: ${command}`);
|
|
193
151
|
});
|
|
194
152
|
|
|
195
|
-
const result = await runUncommittedReview('
|
|
153
|
+
const result = await runUncommittedReview('staged');
|
|
196
154
|
|
|
197
155
|
expect(result).toBeNull();
|
|
198
156
|
});
|
|
@@ -346,7 +304,7 @@ describe('runLocalReview - fork point detection', () => {
|
|
|
346
304
|
throw new Error(`Unmocked command: ${command}`);
|
|
347
305
|
});
|
|
348
306
|
|
|
349
|
-
const result = await runLocalReview(null
|
|
307
|
+
const result = await runLocalReview(null);
|
|
350
308
|
|
|
351
309
|
expect(result).toBeDefined();
|
|
352
310
|
expect(result.source_branch).toBe('feature-branch');
|
|
@@ -396,7 +354,7 @@ describe('runLocalReview - fork point detection', () => {
|
|
|
396
354
|
throw new Error(`Unmocked command: ${command}`);
|
|
397
355
|
});
|
|
398
356
|
|
|
399
|
-
const result = await runLocalReview('main'
|
|
357
|
+
const result = await runLocalReview('main');
|
|
400
358
|
|
|
401
359
|
expect(result).toBeDefined();
|
|
402
360
|
|
|
@@ -571,3 +529,138 @@ describe('shouldIgnoreFile', () => {
|
|
|
571
529
|
expect(shouldIgnoreFile('file.js', pattern)).toBe(false);
|
|
572
530
|
});
|
|
573
531
|
});
|
|
532
|
+
|
|
533
|
+
describe('getContributors', () => {
|
|
534
|
+
beforeEach(() => {
|
|
535
|
+
vi.mock('execa');
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
afterEach(() => {
|
|
539
|
+
vi.restoreAllMocks();
|
|
540
|
+
});
|
|
541
|
+
|
|
542
|
+
it('should extract single contributor with commit count', async () => {
|
|
543
|
+
vi.mocked(execa).mockImplementation(async (cmd, args) => {
|
|
544
|
+
const command = [cmd, ...args].join(' ');
|
|
545
|
+
if (command.includes('log --format=%ae|%an')) {
|
|
546
|
+
return {
|
|
547
|
+
stdout: 'john@example.com|John Doe\njohn@example.com|John Doe\njohn@example.com|John Doe',
|
|
548
|
+
};
|
|
549
|
+
}
|
|
550
|
+
throw new Error(`Unmocked command: ${command}`);
|
|
551
|
+
});
|
|
552
|
+
|
|
553
|
+
const result = await getContributors('abc123..HEAD', '/path/to/repo');
|
|
554
|
+
|
|
555
|
+
expect(result.author_email).toBe('john@example.com');
|
|
556
|
+
expect(result.author_name).toBe('John Doe');
|
|
557
|
+
expect(result.contributors).toHaveLength(1);
|
|
558
|
+
expect(result.contributors[0]).toEqual({
|
|
559
|
+
email: 'john@example.com',
|
|
560
|
+
name: 'John Doe',
|
|
561
|
+
commits: 3,
|
|
562
|
+
});
|
|
563
|
+
});
|
|
564
|
+
|
|
565
|
+
it('should identify author as contributor with most commits', async () => {
|
|
566
|
+
vi.mocked(execa).mockImplementation(async (cmd, args) => {
|
|
567
|
+
const command = [cmd, ...args].join(' ');
|
|
568
|
+
if (command.includes('log --format=%ae|%an')) {
|
|
569
|
+
return {
|
|
570
|
+
stdout: [
|
|
571
|
+
'alice@example.com|Alice Smith',
|
|
572
|
+
'alice@example.com|Alice Smith',
|
|
573
|
+
'alice@example.com|Alice Smith',
|
|
574
|
+
'alice@example.com|Alice Smith',
|
|
575
|
+
'alice@example.com|Alice Smith',
|
|
576
|
+
'bob@example.com|Bob Jones',
|
|
577
|
+
'bob@example.com|Bob Jones',
|
|
578
|
+
'charlie@example.com|Charlie Brown',
|
|
579
|
+
].join('\n'),
|
|
580
|
+
};
|
|
581
|
+
}
|
|
582
|
+
throw new Error(`Unmocked command: ${command}`);
|
|
583
|
+
});
|
|
584
|
+
|
|
585
|
+
const result = await getContributors('abc123..HEAD', '/path/to/repo');
|
|
586
|
+
|
|
587
|
+
expect(result.author_email).toBe('alice@example.com');
|
|
588
|
+
expect(result.author_name).toBe('Alice Smith');
|
|
589
|
+
expect(result.contributors).toHaveLength(3);
|
|
590
|
+
expect(result.contributors[0].commits).toBe(5); // Alice - most commits
|
|
591
|
+
expect(result.contributors[1].commits).toBe(2); // Bob
|
|
592
|
+
expect(result.contributors[2].commits).toBe(1); // Charlie
|
|
593
|
+
});
|
|
594
|
+
|
|
595
|
+
it('should handle empty commit range', async () => {
|
|
596
|
+
vi.mocked(execa).mockImplementation(async (cmd, args) => {
|
|
597
|
+
const command = [cmd, ...args].join(' ');
|
|
598
|
+
if (command.includes('log --format=%ae|%an')) {
|
|
599
|
+
return { stdout: '' };
|
|
600
|
+
}
|
|
601
|
+
throw new Error(`Unmocked command: ${command}`);
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
const result = await getContributors('abc123..HEAD', '/path/to/repo');
|
|
605
|
+
|
|
606
|
+
expect(result.author_email).toBeNull();
|
|
607
|
+
expect(result.author_name).toBeNull();
|
|
608
|
+
expect(result.contributors).toEqual([]);
|
|
609
|
+
});
|
|
610
|
+
|
|
611
|
+
it('should handle git command errors gracefully', async () => {
|
|
612
|
+
vi.mocked(execa).mockImplementation(async () => {
|
|
613
|
+
throw new Error('Git error');
|
|
614
|
+
});
|
|
615
|
+
|
|
616
|
+
const result = await getContributors('abc123..HEAD', '/path/to/repo');
|
|
617
|
+
|
|
618
|
+
expect(result.author_email).toBeNull();
|
|
619
|
+
expect(result.author_name).toBeNull();
|
|
620
|
+
expect(result.contributors).toEqual([]);
|
|
621
|
+
});
|
|
622
|
+
|
|
623
|
+
it('should handle missing name in git log', async () => {
|
|
624
|
+
vi.mocked(execa).mockImplementation(async (cmd, args) => {
|
|
625
|
+
const command = [cmd, ...args].join(' ');
|
|
626
|
+
if (command.includes('log --format=%ae|%an')) {
|
|
627
|
+
return { stdout: 'user@example.com|' };
|
|
628
|
+
}
|
|
629
|
+
throw new Error(`Unmocked command: ${command}`);
|
|
630
|
+
});
|
|
631
|
+
|
|
632
|
+
const result = await getContributors('abc123..HEAD', '/path/to/repo');
|
|
633
|
+
|
|
634
|
+
expect(result.author_email).toBe('user@example.com');
|
|
635
|
+
expect(result.author_name).toBe('user@example.com'); // Falls back to email
|
|
636
|
+
expect(result.contributors[0].name).toBe('user@example.com');
|
|
637
|
+
});
|
|
638
|
+
|
|
639
|
+
it('should sort contributors by commit count descending', async () => {
|
|
640
|
+
vi.mocked(execa).mockImplementation(async (cmd, args) => {
|
|
641
|
+
const command = [cmd, ...args].join(' ');
|
|
642
|
+
if (command.includes('log --format=%ae|%an')) {
|
|
643
|
+
return {
|
|
644
|
+
stdout: [
|
|
645
|
+
'c@example.com|C User',
|
|
646
|
+
'a@example.com|A User',
|
|
647
|
+
'a@example.com|A User',
|
|
648
|
+
'a@example.com|A User',
|
|
649
|
+
'b@example.com|B User',
|
|
650
|
+
'b@example.com|B User',
|
|
651
|
+
].join('\n'),
|
|
652
|
+
};
|
|
653
|
+
}
|
|
654
|
+
throw new Error(`Unmocked command: ${command}`);
|
|
655
|
+
});
|
|
656
|
+
|
|
657
|
+
const result = await getContributors('abc123..HEAD', '/path/to/repo');
|
|
658
|
+
|
|
659
|
+
expect(result.contributors[0].email).toBe('a@example.com');
|
|
660
|
+
expect(result.contributors[0].commits).toBe(3);
|
|
661
|
+
expect(result.contributors[1].email).toBe('b@example.com');
|
|
662
|
+
expect(result.contributors[1].commits).toBe(2);
|
|
663
|
+
expect(result.contributors[2].email).toBe('c@example.com');
|
|
664
|
+
expect(result.contributors[2].commits).toBe(1);
|
|
665
|
+
});
|
|
666
|
+
});
|
package/src/index.js
CHANGED
|
@@ -10,14 +10,7 @@ import { readFileSync } from 'fs';
|
|
|
10
10
|
import { join, dirname } from 'path';
|
|
11
11
|
import { fileURLToPath } from 'url';
|
|
12
12
|
import { runLocalReview } from './git-logic.js';
|
|
13
|
-
import {
|
|
14
|
-
getApiKey,
|
|
15
|
-
setApiKey,
|
|
16
|
-
getApiEndpoint,
|
|
17
|
-
setApiEndpoint,
|
|
18
|
-
getTicketSystem,
|
|
19
|
-
setTicketSystem,
|
|
20
|
-
} from './config.js';
|
|
13
|
+
import { getApiKey, setApiKey, getApiEndpoint, setApiEndpoint } from './config.js';
|
|
21
14
|
import { formatReviewOutput } from './formatter.js';
|
|
22
15
|
|
|
23
16
|
const require = createRequire(import.meta.url);
|
|
@@ -99,18 +92,15 @@ Examples:
|
|
|
99
92
|
$ kk review main Review changes against main branch
|
|
100
93
|
$ kk stg --dry-run Preview staged changes review
|
|
101
94
|
$ kk diff Review unstaged changes
|
|
102
|
-
$ kk all Review all uncommitted changes
|
|
103
95
|
$ kk review main --json Output raw JSON (for CI/CD integration)
|
|
104
96
|
|
|
105
97
|
Common Options:
|
|
106
98
|
--dry-run Show payload without sending to API
|
|
107
99
|
--json Output raw API response as JSON
|
|
108
|
-
--ticket-system <system> Use specific ticket system (jira or ado)
|
|
109
100
|
|
|
110
101
|
Configuration:
|
|
111
102
|
$ kk config --key YOUR_KEY
|
|
112
|
-
$ kk config --endpoint https://api.korekt.ai/review
|
|
113
|
-
$ kk config --ticket-system ado
|
|
103
|
+
$ kk config --endpoint https://api.korekt.ai/api/review
|
|
114
104
|
|
|
115
105
|
CI/CD Integration:
|
|
116
106
|
$ kk get-script github Output GitHub Actions integration script
|
|
@@ -126,7 +116,6 @@ program
|
|
|
126
116
|
'[target-branch]',
|
|
127
117
|
'The branch to compare against (e.g., main, develop). If not specified, auto-detects fork point.'
|
|
128
118
|
)
|
|
129
|
-
.option('--ticket-system <system>', 'Ticket system to use (jira or ado)')
|
|
130
119
|
.option('--dry-run', 'Show payload without sending to API')
|
|
131
120
|
.option(
|
|
132
121
|
'--ignore <patterns...>',
|
|
@@ -153,30 +142,14 @@ program
|
|
|
153
142
|
process.exit(1);
|
|
154
143
|
}
|
|
155
144
|
|
|
156
|
-
// Determine ticket system to use (or null if not configured)
|
|
157
|
-
const ticketSystem = options.ticketSystem || getTicketSystem() || null;
|
|
158
|
-
|
|
159
|
-
// Validate ticket system
|
|
160
|
-
if (ticketSystem && !['jira', 'ado'].includes(ticketSystem.toLowerCase())) {
|
|
161
|
-
log(chalk.red(`Invalid ticket system: ${ticketSystem}`));
|
|
162
|
-
log(chalk.gray('Valid options: jira, ado'));
|
|
163
|
-
process.exit(1);
|
|
164
|
-
}
|
|
165
|
-
|
|
166
145
|
// Gather all data using our git logic module
|
|
167
|
-
const payload = await runLocalReview(targetBranch,
|
|
146
|
+
const payload = await runLocalReview(targetBranch, options.ignore);
|
|
168
147
|
|
|
169
148
|
if (!payload) {
|
|
170
149
|
log(chalk.red('Could not proceed with review due to errors during analysis.'));
|
|
171
150
|
process.exit(1);
|
|
172
151
|
}
|
|
173
152
|
|
|
174
|
-
// Add ticket system to payload if specified
|
|
175
|
-
if (ticketSystem) {
|
|
176
|
-
payload.ticket_system = ticketSystem;
|
|
177
|
-
log(chalk.gray(`Using ticket system: ${ticketSystem}`));
|
|
178
|
-
}
|
|
179
|
-
|
|
180
153
|
// If dry-run, just show the payload and exit
|
|
181
154
|
if (options.dryRun) {
|
|
182
155
|
log(chalk.yellow('\nš Dry Run - Payload that would be sent:\n'));
|
|
@@ -200,7 +173,7 @@ program
|
|
|
200
173
|
log(` Commits: ${chalk.cyan(payload.commit_messages.length)}`);
|
|
201
174
|
log(` Files: ${chalk.cyan(payload.changed_files.length)}\n`);
|
|
202
175
|
|
|
203
|
-
log(chalk.bold(
|
|
176
|
+
log(chalk.bold(` ${payload.changed_files.length} files to review:`));
|
|
204
177
|
payload.changed_files.forEach((file) => {
|
|
205
178
|
const statusColor =
|
|
206
179
|
{
|
|
@@ -276,7 +249,6 @@ program
|
|
|
276
249
|
.command('review-staged')
|
|
277
250
|
.aliases(['stg', 'staged', 'cached'])
|
|
278
251
|
.description('Review staged changes (git diff --cached)')
|
|
279
|
-
.option('--ticket-system <system>', 'Ticket system to use (jira or ado)')
|
|
280
252
|
.option('--dry-run', 'Show payload without sending to API')
|
|
281
253
|
.option('--json', 'Output raw API response as JSON')
|
|
282
254
|
.action(async (options) => {
|
|
@@ -288,28 +260,13 @@ program
|
|
|
288
260
|
.command('review-unstaged')
|
|
289
261
|
.alias('diff')
|
|
290
262
|
.description('Review unstaged changes (git diff)')
|
|
291
|
-
.option('--ticket-system <system>', 'Ticket system to use (jira or ado)')
|
|
292
263
|
.option('--dry-run', 'Show payload without sending to API')
|
|
293
|
-
.option('--untracked', 'Include untracked files in the review')
|
|
294
264
|
.option('--json', 'Output raw API response as JSON')
|
|
295
265
|
.action(async (options) => {
|
|
296
266
|
log(chalk.blue.bold('š Reviewing unstaged changes...'));
|
|
297
267
|
await reviewUncommitted('unstaged', options);
|
|
298
268
|
});
|
|
299
269
|
|
|
300
|
-
program
|
|
301
|
-
.command('review-all-uncommitted')
|
|
302
|
-
.alias('all')
|
|
303
|
-
.description('Review all uncommitted changes (staged + unstaged)')
|
|
304
|
-
.option('--ticket-system <system>', 'Ticket system to use (jira or ado)')
|
|
305
|
-
.option('--dry-run', 'Show payload without sending to API')
|
|
306
|
-
.option('--untracked', 'Include untracked files in the review')
|
|
307
|
-
.option('--json', 'Output raw API response as JSON')
|
|
308
|
-
.action(async (options) => {
|
|
309
|
-
log(chalk.blue.bold('š Reviewing all uncommitted changes...'));
|
|
310
|
-
await reviewUncommitted('all', options);
|
|
311
|
-
});
|
|
312
|
-
|
|
313
270
|
async function reviewUncommitted(mode, options) {
|
|
314
271
|
const apiKey = getApiKey();
|
|
315
272
|
if (!apiKey) {
|
|
@@ -325,27 +282,14 @@ async function reviewUncommitted(mode, options) {
|
|
|
325
282
|
process.exit(1);
|
|
326
283
|
}
|
|
327
284
|
|
|
328
|
-
const ticketSystem = options.ticketSystem || getTicketSystem() || null;
|
|
329
|
-
|
|
330
|
-
if (ticketSystem && !['jira', 'ado'].includes(ticketSystem.toLowerCase())) {
|
|
331
|
-
log(chalk.red(`Invalid ticket system: ${ticketSystem}`));
|
|
332
|
-
log(chalk.gray('Valid options: jira, ado'));
|
|
333
|
-
process.exit(1);
|
|
334
|
-
}
|
|
335
|
-
|
|
336
285
|
const { runUncommittedReview } = await import('./git-logic.js');
|
|
337
|
-
const payload = await runUncommittedReview(mode
|
|
286
|
+
const payload = await runUncommittedReview(mode);
|
|
338
287
|
|
|
339
288
|
if (!payload) {
|
|
340
289
|
log(chalk.red('No changes found or error occurred during analysis.'));
|
|
341
290
|
process.exit(1);
|
|
342
291
|
}
|
|
343
292
|
|
|
344
|
-
if (ticketSystem) {
|
|
345
|
-
payload.ticket_system = ticketSystem;
|
|
346
|
-
log(chalk.gray(`Using ticket system: ${ticketSystem}`));
|
|
347
|
-
}
|
|
348
|
-
|
|
349
293
|
if (options.dryRun) {
|
|
350
294
|
log(chalk.yellow('\nš Dry Run - Payload that would be sent:\n'));
|
|
351
295
|
|
|
@@ -364,7 +308,7 @@ async function reviewUncommitted(mode, options) {
|
|
|
364
308
|
if (!options.json) {
|
|
365
309
|
log(chalk.yellow('\nš Ready to submit uncommitted changes for review:\n'));
|
|
366
310
|
log(chalk.gray(' Comparing against HEAD (last commit)\n'));
|
|
367
|
-
log(chalk.bold(
|
|
311
|
+
log(chalk.bold(` ${payload.changed_files.length} files to review:`));
|
|
368
312
|
payload.changed_files.forEach((file) => {
|
|
369
313
|
const statusColor =
|
|
370
314
|
{
|
|
@@ -440,22 +384,17 @@ program
|
|
|
440
384
|
.description('Configure API settings')
|
|
441
385
|
.option('--key <key>', 'Your API key')
|
|
442
386
|
.option('--endpoint <endpoint>', 'Your API endpoint URL')
|
|
443
|
-
.option('--ticket-system <system>', 'Ticket system (jira, ado)')
|
|
444
387
|
.option('--show', 'Show current configuration')
|
|
445
388
|
.action((options) => {
|
|
446
389
|
// Show current config if --show flag is used
|
|
447
390
|
if (options.show) {
|
|
448
391
|
const apiKey = getApiKey();
|
|
449
392
|
const apiEndpoint = getApiEndpoint();
|
|
450
|
-
const ticketSystem = getTicketSystem();
|
|
451
393
|
|
|
452
394
|
console.log(chalk.bold('\nCurrent Configuration:\n'));
|
|
453
395
|
console.log(` API Key: ${apiKey ? chalk.green('ā Set') : chalk.red('ā Not set')}`);
|
|
454
396
|
console.log(
|
|
455
|
-
` API Endpoint: ${apiEndpoint ? chalk.cyan(apiEndpoint) : chalk.red('ā Not set')}`
|
|
456
|
-
);
|
|
457
|
-
console.log(
|
|
458
|
-
` Ticket System: ${ticketSystem ? chalk.cyan(ticketSystem) : chalk.gray('Not configured')}\n`
|
|
397
|
+
` API Endpoint: ${apiEndpoint ? chalk.cyan(apiEndpoint) : chalk.red('ā Not set')}\n`
|
|
459
398
|
);
|
|
460
399
|
return;
|
|
461
400
|
}
|
|
@@ -476,29 +415,11 @@ program
|
|
|
476
415
|
setApiEndpoint(options.endpoint);
|
|
477
416
|
console.log(chalk.green('ā API Endpoint saved successfully!'));
|
|
478
417
|
}
|
|
479
|
-
if (options.
|
|
480
|
-
if (options.ticketSystem === '') {
|
|
481
|
-
// Clear ticket system
|
|
482
|
-
setTicketSystem(null);
|
|
483
|
-
console.log(chalk.green('ā Ticket System cleared!'));
|
|
484
|
-
} else {
|
|
485
|
-
// Validate ticket system
|
|
486
|
-
const validSystems = ['jira', 'ado'];
|
|
487
|
-
if (!validSystems.includes(options.ticketSystem.toLowerCase())) {
|
|
488
|
-
console.error(chalk.red(`Invalid ticket system: ${options.ticketSystem}`));
|
|
489
|
-
console.error(chalk.gray(`Valid options: ${validSystems.join(', ')}`));
|
|
490
|
-
return;
|
|
491
|
-
}
|
|
492
|
-
setTicketSystem(options.ticketSystem);
|
|
493
|
-
console.log(chalk.green('ā Ticket System saved successfully!'));
|
|
494
|
-
}
|
|
495
|
-
}
|
|
496
|
-
if (!options.key && !options.endpoint && options.ticketSystem === undefined && !options.show) {
|
|
418
|
+
if (!options.key && !options.endpoint && !options.show) {
|
|
497
419
|
console.log(chalk.yellow('Please provide at least one configuration option.'));
|
|
498
420
|
console.log('\nUsage:');
|
|
499
421
|
console.log(' kk config --key YOUR_API_KEY');
|
|
500
|
-
console.log(' kk config --endpoint https://api.korekt.ai/review
|
|
501
|
-
console.log(' kk config --ticket-system jira');
|
|
422
|
+
console.log(' kk config --endpoint https://api.korekt.ai/api/review');
|
|
502
423
|
console.log(' kk config --show (view current configuration)');
|
|
503
424
|
}
|
|
504
425
|
});
|