korekt-cli 0.2.0 → 0.4.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Vladan Djokic
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,6 +1,10 @@
1
1
  # Korekt CLI
2
2
 
3
- AI-powered code review CLI - Keep your kode korekt
3
+ [![npm version](https://img.shields.io/npm/v/korekt-cli.svg)](https://www.npmjs.com/package/korekt-cli)
4
+ [![npm downloads](https://img.shields.io/npm/dm/korekt-cli.svg)](https://www.npmjs.com/package/korekt-cli)
5
+ [![license](https://img.shields.io/npm/l/korekt-cli.svg)](https://www.npmjs.com/package/korekt-cli)
6
+
7
+ AI-powered code review CLI - Keep your kode korekt
4
8
 
5
9
  `kk` integrates seamlessly with your local Git workflow to provide intelligent code reviews powered by AI.
6
10
 
@@ -128,3 +132,9 @@ To run tests:
128
132
  ```bash
129
133
  npm test
130
134
  ```
135
+
136
+ ## License
137
+
138
+ MIT © [Vladan Djokic](https://korekt.ai)
139
+
140
+ See [LICENSE](./LICENSE) for details.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "korekt-cli",
3
- "version": "0.2.0",
3
+ "version": "0.4.0",
4
4
  "description": "AI-powered code review CLI - Keep your kode korekt",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -11,7 +11,18 @@
11
11
  "scripts": {
12
12
  "test": "vitest",
13
13
  "test:watch": "vitest --watch",
14
- "test:coverage": "vitest --coverage"
14
+ "test:coverage": "vitest --coverage",
15
+ "lint": "eslint src/**/*.js",
16
+ "lint:fix": "eslint src/**/*.js --fix",
17
+ "format": "prettier --write \"src/**/*.js\"",
18
+ "format:check": "prettier --check \"src/**/*.js\"",
19
+ "prepare": "husky"
20
+ },
21
+ "lint-staged": {
22
+ "src/**/*.js": [
23
+ "eslint --fix",
24
+ "prettier --write"
25
+ ]
15
26
  },
16
27
  "keywords": [
17
28
  "code-review",
@@ -34,7 +45,7 @@
34
45
  "files": [
35
46
  "src"
36
47
  ],
37
- "license": "UNLICENSED",
48
+ "license": "MIT",
38
49
  "dependencies": {
39
50
  "axios": "^1.12.2",
40
51
  "chalk": "^5.6.2",
@@ -45,6 +56,11 @@
45
56
  "ora": "^9.0.0"
46
57
  },
47
58
  "devDependencies": {
59
+ "@eslint/js": "^9.38.0",
60
+ "eslint": "^9.38.0",
61
+ "husky": "^9.1.7",
62
+ "lint-staged": "^16.2.6",
63
+ "prettier": "^3.6.2",
48
64
  "vitest": "^3.2.4"
49
65
  }
50
66
  }
package/src/config.js CHANGED
@@ -72,4 +72,4 @@ export function getConfig() {
72
72
  apiEndpoint: getApiEndpoint(),
73
73
  ticketSystem: getTicketSystem(),
74
74
  };
75
- }
75
+ }
package/src/formatter.js CHANGED
@@ -39,9 +39,7 @@ const CATEGORY_ICONS = {
39
39
  */
40
40
  function formatCategory(category) {
41
41
  if (!category) return '';
42
- return category
43
- .replace(/_/g, ' ')
44
- .replace(/\b\w/g, char => char.toUpperCase());
42
+ return category.replace(/_/g, ' ').replace(/\b\w/g, (char) => char.toUpperCase());
45
43
  }
46
44
 
47
45
  /**
@@ -52,7 +50,7 @@ function getGitRoot() {
52
50
  try {
53
51
  const { stdout } = execaSync('git', ['rev-parse', '--show-toplevel']);
54
52
  return stdout.trim();
55
- } catch (error) {
53
+ } catch {
56
54
  // Fallback to current working directory if not in a git repo
57
55
  return process.cwd();
58
56
  }
@@ -83,11 +81,12 @@ export function formatReviewOutput(data) {
83
81
  // --- Praises Section ---
84
82
  if (review && review.praises && review.praises.length > 0) {
85
83
  console.log(chalk.bold.magenta(`✨ Praises (${summary.total_praises})`));
86
- review.praises.forEach(praise => {
87
- const categoryIcon = CATEGORY_ICONS[praise.category] || CATEGORY_ICONS.default;
84
+ review.praises.forEach((praise) => {
88
85
  const formattedCategory = formatCategory(praise.category);
89
86
  const absolutePath = toAbsolutePath(praise.file_path);
90
- console.log(` ✅ ${chalk.green.bold(formattedCategory)} in ${absolutePath}:${praise.line_number}`);
87
+ console.log(
88
+ ` ✅ ${chalk.green.bold(formattedCategory)} in ${absolutePath}:${praise.line_number}`
89
+ );
91
90
  console.log(` ${praise.message}\n`);
92
91
  });
93
92
  }
@@ -99,7 +98,7 @@ export function formatReviewOutput(data) {
99
98
  // Severity Summary Table
100
99
  console.log(chalk.underline('Severity Count:'));
101
100
  const severities = ['critical', 'high', 'medium', 'low'];
102
- severities.forEach(severity => {
101
+ severities.forEach((severity) => {
103
102
  const count = summary[severity] || 0;
104
103
  if (count > 0) {
105
104
  const icon = SEVERITY_ICONS[severity];
@@ -128,7 +127,10 @@ export function formatReviewOutput(data) {
128
127
  if (issue.suggested_fix) {
129
128
  console.log(chalk.bold('\n💡 Suggested Fix:'));
130
129
  // Indent the suggested fix for readability
131
- const indentedFix = issue.suggested_fix.split('\n').map(line => ` ${line}`).join('\n');
130
+ const indentedFix = issue.suggested_fix
131
+ .split('\n')
132
+ .map((line) => ` ${line}`)
133
+ .join('\n');
132
134
  console.log(chalk.green(indentedFix));
133
135
  }
134
136
 
package/src/git-logic.js CHANGED
@@ -28,21 +28,21 @@ export function truncateContent(content, maxLines = 2000) {
28
28
  */
29
29
  export function normalizeRepoUrl(url) {
30
30
  // Handle Azure DevOps SSH format: git@ssh.dev.azure.com:v3/org/project/repo
31
- const azureDevOpsSshMatch = url.match(/git@ssh\.dev\.azure\.com:v3\/([^\/]+)\/([^\/]+)\/(.+)/);
31
+ const azureDevOpsSshMatch = url.match(/git@ssh\.dev\.azure\.com:v3\/([^/]+)\/([^/]+)\/(.+)/);
32
32
  if (azureDevOpsSshMatch) {
33
33
  const [, org, project, repo] = azureDevOpsSshMatch;
34
34
  return `https://dev.azure.com/${org}/${project}/_git/${repo}`;
35
35
  }
36
36
 
37
37
  // Handle GitHub SSH format: git@github.com:user/repo.git
38
- const githubSshMatch = url.match(/git@github\.com:([^\/]+)\/(.+?)(?:\.git)?$/);
38
+ const githubSshMatch = url.match(/git@github\.com:([^/]+)\/(.+?)(?:\.git)?$/);
39
39
  if (githubSshMatch) {
40
40
  const [, user, repo] = githubSshMatch;
41
41
  return `https://github.com/${user}/${repo}`;
42
42
  }
43
43
 
44
44
  // Handle GitLab SSH format: git@gitlab.com:user/repo.git
45
- const gitlabSshMatch = url.match(/git@gitlab\.com:([^\/]+)\/(.+?)(?:\.git)?$/);
45
+ const gitlabSshMatch = url.match(/git@gitlab\.com:([^/]+)\/(.+?)(?:\.git)?$/);
46
46
  if (gitlabSshMatch) {
47
47
  const [, user, repo] = gitlabSshMatch;
48
48
  return `https://gitlab.com/${user}/${repo}`;
@@ -69,11 +69,11 @@ export function shouldIgnoreFile(filePath, patterns) {
69
69
  // Replace * with [^/]* (matches anything except /)
70
70
  // Replace ** with .* (matches anything including /)
71
71
  let regexPattern = pattern
72
- .replace(/\./g, '\\.') // Escape dots
73
- .replace(/\*\*/g, '___DOUBLESTAR___') // Temporarily replace **
74
- .replace(/\*/g, '[^/]*') // Replace single * with [^/]*
75
- .replace(/___DOUBLESTAR___/g, '.*') // Replace ** with .*
76
- .replace(/\?/g, '.'); // Replace ? with .
72
+ .replace(/\./g, '\\.') // Escape dots
73
+ .replace(/\*\*/g, '___DOUBLESTAR___') // Temporarily replace **
74
+ .replace(/\*/g, '[^/]*') // Replace single * with [^/]*
75
+ .replace(/___DOUBLESTAR___/g, '.*') // Replace ** with .*
76
+ .replace(/\?/g, '.'); // Replace ? with .
77
77
 
78
78
  // Handle leading **/ pattern - make it optional so it matches both with and without directory prefix
79
79
  // For example, **/*.sql should match both "file.sql" and "dir/file.sql"
@@ -133,7 +133,11 @@ export function parseNameStatus(output) {
133
133
  * @param {string|null} ticketSystem - The ticket system to use (jira or ado), or null to skip ticket extraction
134
134
  * @returns {Object|null} - The payload object ready for API submission, or null on error
135
135
  */
136
- export async function runUncommittedReview(mode = 'all', ticketSystem = null, includeUntracked = false) {
136
+ export async function runUncommittedReview(
137
+ mode = 'all',
138
+ _ticketSystem = null,
139
+ includeUntracked = false
140
+ ) {
137
141
  try {
138
142
  // 1. Get Repo URL and current branch name
139
143
  const { stdout: repoUrl } = await execa('git', ['remote', 'get-url', 'origin']);
@@ -164,12 +168,19 @@ export async function runUncommittedReview(mode = 'all', ticketSystem = null, in
164
168
  // Handle untracked files if requested
165
169
  if (includeUntracked) {
166
170
  console.log(chalk.gray('Analyzing untracked files...'));
167
- const { stdout: untrackedFilesOutput } = await execa('git', ['ls-files', '--others', '--exclude-standard']);
171
+ const { stdout: untrackedFilesOutput } = await execa('git', [
172
+ 'ls-files',
173
+ '--others',
174
+ '--exclude-standard',
175
+ ]);
168
176
  const untrackedFiles = untrackedFilesOutput.split('\n').filter(Boolean);
169
177
 
170
178
  for (const file of untrackedFiles) {
171
179
  const content = fs.readFileSync(file, 'utf-8');
172
- const diff = content.split('\n').map(line => `+${line}`).join('\n');
180
+ const diff = content
181
+ .split('\n')
182
+ .map((line) => `+${line}`)
183
+ .join('\n');
173
184
  changedFiles.push({
174
185
  path: file,
175
186
  status: 'A', // Untracked files are always additions
@@ -182,8 +193,8 @@ export async function runUncommittedReview(mode = 'all', ticketSystem = null, in
182
193
  }
183
194
 
184
195
  // Deduplicate file list before processing diffs
185
- const processedPaths = new Set(changedFiles.map(f => f.path));
186
- const uniqueFileList = fileList.filter(file => !processedPaths.has(file.path));
196
+ const processedPaths = new Set(changedFiles.map((f) => f.path));
197
+ const uniqueFileList = fileList.filter((file) => !processedPaths.has(file.path));
187
198
 
188
199
  for (const file of uniqueFileList) {
189
200
  const { status, path, oldPath } = file;
@@ -199,7 +210,13 @@ export async function runUncommittedReview(mode = 'all', ticketSystem = null, in
199
210
  } else {
200
211
  // For 'all', try staged first, then unstaged
201
212
  try {
202
- const { stdout: stagedDiff } = await execa('git', ['diff', '--cached', '-U15', '--', path]);
213
+ const { stdout: stagedDiff } = await execa('git', [
214
+ 'diff',
215
+ '--cached',
216
+ '-U15',
217
+ '--',
218
+ path,
219
+ ]);
203
220
  if (stagedDiff) {
204
221
  diff = stagedDiff;
205
222
  } else {
@@ -218,8 +235,10 @@ export async function runUncommittedReview(mode = 'all', ticketSystem = null, in
218
235
  try {
219
236
  const { stdout: headContent } = await execa('git', ['show', `HEAD:${oldPath}`]);
220
237
  content = headContent;
221
- } catch (e) {
222
- console.warn(chalk.yellow(`Could not get HEAD content for ${oldPath}. Assuming it's new.`));
238
+ } catch {
239
+ console.warn(
240
+ chalk.yellow(`Could not get HEAD content for ${oldPath}. Assuming it's new.`)
241
+ );
223
242
  }
224
243
  }
225
244
 
@@ -268,7 +287,11 @@ export async function runUncommittedReview(mode = 'all', ticketSystem = null, in
268
287
  * @param {string[]|null} ignorePatterns - Array of glob patterns to ignore files
269
288
  * @returns {Object|null} - The payload object ready for API submission, or null on error
270
289
  */
271
- export async function runLocalReview(targetBranch = null, ticketSystem = null, ignorePatterns = null) {
290
+ export async function runLocalReview(
291
+ targetBranch = null,
292
+ _ticketSystem = null,
293
+ ignorePatterns = null
294
+ ) {
272
295
  try {
273
296
  // 1. Get Repo URL, current branch name, and repository root
274
297
  const { stdout: repoUrl } = await execa('git', ['remote', 'get-url', 'origin']);
@@ -285,7 +308,7 @@ export async function runLocalReview(targetBranch = null, ticketSystem = null, i
285
308
  // Check if the branch exists locally
286
309
  try {
287
310
  await execa('git', ['rev-parse', '--verify', targetBranch]);
288
- } catch (error) {
311
+ } catch {
289
312
  console.error(chalk.red(`Branch '${targetBranch}' does not exist locally.`));
290
313
  console.error(chalk.gray(`Please check out the branch first or specify a different one.`));
291
314
  return null;
@@ -299,8 +322,10 @@ export async function runLocalReview(targetBranch = null, ticketSystem = null, i
299
322
  // If fetch succeeded, use the remote-tracking branch for comparison
300
323
  // This is safer as it doesn't modify the user's local branch
301
324
  targetBranchRef = `origin/${targetBranch}`;
302
- console.log(chalk.gray(`Using remote-tracking branch 'origin/${targetBranch}' for comparison.`));
303
- } catch (fetchError) {
325
+ console.log(
326
+ chalk.gray(`Using remote-tracking branch 'origin/${targetBranch}' for comparison.`)
327
+ );
328
+ } catch {
304
329
  console.warn(chalk.yellow(`Could not fetch remote branch 'origin/${targetBranch}'.`));
305
330
  console.warn(chalk.gray(`Proceeding with local branch '${targetBranch}' for comparison.`));
306
331
  // targetBranchRef stays as targetBranch (local branch)
@@ -313,7 +338,12 @@ export async function runLocalReview(targetBranch = null, ticketSystem = null, i
313
338
  if (!targetBranch) {
314
339
  try {
315
340
  // Use git reflog to find where the branch was created
316
- const { stdout: reflog } = await execa('git', ['reflog', 'show', '--no-abbrev-commit', branchName]);
341
+ const { stdout: reflog } = await execa('git', [
342
+ 'reflog',
343
+ 'show',
344
+ '--no-abbrev-commit',
345
+ branchName,
346
+ ]);
317
347
  const lines = reflog.split('\n');
318
348
 
319
349
  // Look for the branch creation point (last line in reflog)
@@ -322,15 +352,19 @@ export async function runLocalReview(targetBranch = null, ticketSystem = null, i
322
352
  const match = creationLine.match(/^([a-f0-9]{40})/);
323
353
  if (match) {
324
354
  mergeBase = match[1];
325
- console.log(chalk.gray(`Auto-detected fork point from reflog: ${mergeBase.substring(0, 7)}`));
355
+ console.log(
356
+ chalk.gray(`Auto-detected fork point from reflog: ${mergeBase.substring(0, 7)}`)
357
+ );
326
358
  }
327
359
  }
328
360
 
329
361
  if (!mergeBase) {
330
362
  throw new Error('Could not find fork point in reflog');
331
363
  }
332
- } catch (error) {
333
- console.error(chalk.red('Could not auto-detect fork point. Please specify a target branch.'));
364
+ } catch {
365
+ console.error(
366
+ chalk.red('Could not auto-detect fork point. Please specify a target branch.')
367
+ );
334
368
  console.error(chalk.gray('Usage: kk review <target-branch>'));
335
369
  return null;
336
370
  }
@@ -349,21 +383,25 @@ export async function runLocalReview(targetBranch = null, ticketSystem = null, i
349
383
  console.log(chalk.gray(`Analyzing commits from ${mergeBase.substring(0, 7)} to HEAD...`));
350
384
 
351
385
  // 3. Get Commit Messages with proper delimiter
352
- const { stdout: logOutput } = await execa('git', ['log', '--pretty=%B---EOC---', diffRange], { cwd: repoRootPath });
386
+ const { stdout: logOutput } = await execa('git', ['log', '--pretty=%B---EOC---', diffRange], {
387
+ cwd: repoRootPath,
388
+ });
353
389
  const commitMessages = logOutput
354
390
  .split('---EOC---')
355
391
  .map((msg) => msg.trim())
356
392
  .filter(Boolean);
357
393
 
358
394
  // 4. Get changed files and their status
359
- const { stdout: nameStatusOutput } = await execa('git', ['diff', '--name-status', diffRange], { cwd: repoRootPath });
395
+ const { stdout: nameStatusOutput } = await execa('git', ['diff', '--name-status', diffRange], {
396
+ cwd: repoRootPath,
397
+ });
360
398
  const fileList = parseNameStatus(nameStatusOutput);
361
399
 
362
400
  // Filter out ignored files
363
401
  let filteredFileList = fileList;
364
402
  let ignoredCount = 0;
365
403
  if (ignorePatterns && ignorePatterns.length > 0) {
366
- filteredFileList = fileList.filter(file => {
404
+ filteredFileList = fileList.filter((file) => {
367
405
  const ignored = shouldIgnoreFile(file.path, ignorePatterns);
368
406
  if (ignored) {
369
407
  ignoredCount++;
@@ -385,19 +423,22 @@ export async function runLocalReview(targetBranch = null, ticketSystem = null, i
385
423
 
386
424
  // Run git commands from the repository root to handle all file paths correctly
387
425
  // This works regardless of whether we're in a subdirectory or at the repo root
388
- const { stdout: diff } = await execa('git', ['diff', '-U15', diffRange, '--', path], { cwd: repoRootPath });
426
+ const { stdout: diff } = await execa('git', ['diff', '-U15', diffRange, '--', path], {
427
+ cwd: repoRootPath,
428
+ });
389
429
 
390
430
  // Get the original content from the base commit
391
431
  let content = '';
392
432
  if (status !== 'A') {
393
433
  // Added files have no original content
394
434
  try {
395
- const { stdout: originalContent } = await execa('git', [
396
- 'show',
397
- `${mergeBase.trim()}:${oldPath}`,
398
- ], { cwd: repoRootPath });
435
+ const { stdout: originalContent } = await execa(
436
+ 'git',
437
+ ['show', `${mergeBase.trim()}:${oldPath}`],
438
+ { cwd: repoRootPath }
439
+ );
399
440
  content = originalContent;
400
- } catch (e) {
441
+ } catch {
401
442
  // This can happen if a file was added and modified in the same branch
402
443
  console.warn(
403
444
  chalk.yellow(`Could not get original content for ${oldPath}. Assuming it was added.`)
@@ -437,4 +478,4 @@ export async function runLocalReview(targetBranch = null, ticketSystem = null, i
437
478
  }
438
479
  return null; // Return null to indicate failure
439
480
  }
440
- }
481
+ }
@@ -1,9 +1,6 @@
1
1
  import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
2
  import {
3
3
  parseNameStatus,
4
- findJiraTicketIds,
5
- findAdoTicketIds,
6
- extractTicketIds,
7
4
  runLocalReview,
8
5
  runUncommittedReview,
9
6
  truncateContent,
@@ -221,14 +218,17 @@ describe('runLocalReview - branch fetching', () => {
221
218
 
222
219
  const result = await runLocalReview('non-existent-branch');
223
220
  expect(result).toBeNull();
224
- expect(console.error).toHaveBeenCalledWith(expect.stringContaining("Branch 'non-existent-branch' does not exist locally."));
221
+ expect(console.error).toHaveBeenCalledWith(
222
+ expect.stringContaining("Branch 'non-existent-branch' does not exist locally.")
223
+ );
225
224
  });
226
225
 
227
226
  it('should fetch latest changes if target branch exists locally', async () => {
228
227
  vi.mocked(execa).mockImplementation(async (cmd, args) => {
229
228
  const command = [cmd, ...args].join(' ');
230
229
  // Common setup
231
- if (command.includes('remote get-url origin')) return { stdout: 'https://github.com/user/repo.git' };
230
+ if (command.includes('remote get-url origin'))
231
+ return { stdout: 'https://github.com/user/repo.git' };
232
232
  if (command.includes('rev-parse --abbrev-ref HEAD')) return { stdout: 'current-branch' };
233
233
  if (command.includes('rev-parse --show-toplevel')) return { stdout: '/path/to/repo' };
234
234
 
@@ -249,14 +249,15 @@ describe('runLocalReview - branch fetching', () => {
249
249
  const result = await runLocalReview('main');
250
250
  expect(result).not.toBeNull();
251
251
  const execaCalls = vi.mocked(execa).mock.calls;
252
- const fetchCall = execaCalls.find(call => call[0] === 'git' && call[1].includes('fetch'));
252
+ const fetchCall = execaCalls.find((call) => call[0] === 'git' && call[1].includes('fetch'));
253
253
  expect(fetchCall).toBeDefined();
254
254
  });
255
255
 
256
256
  it('should warn and continue if fetch fails', async () => {
257
257
  vi.mocked(execa).mockImplementation(async (cmd, args) => {
258
258
  const command = [cmd, ...args].join(' ');
259
- if (command.includes('remote get-url origin')) return { stdout: 'https://github.com/user/repo.git' };
259
+ if (command.includes('remote get-url origin'))
260
+ return { stdout: 'https://github.com/user/repo.git' };
260
261
  if (command.includes('rev-parse --abbrev-ref HEAD')) return { stdout: 'current-branch' };
261
262
  if (command.includes('rev-parse --show-toplevel')) return { stdout: '/path/to/repo' };
262
263
  if (command.includes('rev-parse --verify main')) return { stdout: 'commit-hash' };
@@ -278,8 +279,12 @@ describe('runLocalReview - branch fetching', () => {
278
279
 
279
280
  const result = await runLocalReview('main');
280
281
  expect(result).not.toBeNull(); // Should still proceed
281
- expect(console.warn).toHaveBeenCalledWith(expect.stringContaining("Could not fetch remote branch 'origin/main'."));
282
- expect(console.warn).toHaveBeenCalledWith(expect.stringContaining("Proceeding with local branch 'main' for comparison."));
282
+ expect(console.warn).toHaveBeenCalledWith(
283
+ expect.stringContaining("Could not fetch remote branch 'origin/main'.")
284
+ );
285
+ expect(console.warn).toHaveBeenCalledWith(
286
+ expect.stringContaining("Proceeding with local branch 'main' for comparison.")
287
+ );
283
288
  });
284
289
  });
285
290
 
@@ -308,7 +313,10 @@ describe('runLocalReview - fork point detection', () => {
308
313
  }
309
314
  if (command.includes('reflog show --no-abbrev-commit feature-branch')) {
310
315
  // Simulate reflog output - last line is where branch was created
311
- return { stdout: 'abc123def456 feature-branch@{0}: commit: latest\nfedcba654321 feature-branch@{1}: commit: middle\n510572bc5197788770004d0d0585822adab0128f feature-branch@{2}: branch: Created from master' };
316
+ return {
317
+ stdout:
318
+ 'abc123def456 feature-branch@{0}: commit: latest\nfedcba654321 feature-branch@{1}: commit: middle\n510572bc5197788770004d0d0585822adab0128f feature-branch@{2}: branch: Created from master',
319
+ };
312
320
  }
313
321
  if (command.includes('log --pretty=%B---EOC---') && command.includes('510572bc')) {
314
322
  return { stdout: 'feat: add feature---EOC---' };
@@ -333,9 +341,7 @@ describe('runLocalReview - fork point detection', () => {
333
341
 
334
342
  // Should have used reflog to find fork point
335
343
  const execaCalls = vi.mocked(execa).mock.calls;
336
- const reflogCall = execaCalls.find(call =>
337
- call[0] === 'git' && call[1].includes('reflog')
338
- );
344
+ const reflogCall = execaCalls.find((call) => call[0] === 'git' && call[1].includes('reflog'));
339
345
  expect(reflogCall).toBeDefined();
340
346
  });
341
347
 
@@ -384,8 +390,9 @@ describe('runLocalReview - fork point detection', () => {
384
390
 
385
391
  // Should have used merge-base with origin/main (since fetch succeeded)
386
392
  const execaCalls = vi.mocked(execa).mock.calls;
387
- const mergeBaseCall = execaCalls.find(call =>
388
- call[0] === 'git' && call[1].includes('merge-base') && call[1].includes('origin/main')
393
+ const mergeBaseCall = execaCalls.find(
394
+ (call) =>
395
+ call[0] === 'git' && call[1].includes('merge-base') && call[1].includes('origin/main')
389
396
  );
390
397
  expect(mergeBaseCall).toBeDefined();
391
398
  });
@@ -539,4 +546,4 @@ describe('shouldIgnoreFile', () => {
539
546
  expect(shouldIgnoreFile('file.sql', pattern)).toBe(true);
540
547
  expect(shouldIgnoreFile('file.js', pattern)).toBe(false);
541
548
  });
542
- });
549
+ });
package/src/index.js CHANGED
@@ -6,7 +6,14 @@ import chalk from 'chalk';
6
6
  import readline from 'readline';
7
7
  import ora from 'ora';
8
8
  import { runLocalReview } from './git-logic.js';
9
- import { getApiKey, setApiKey, getApiEndpoint, setApiEndpoint, getTicketSystem, setTicketSystem } from './config.js';
9
+ import {
10
+ getApiKey,
11
+ setApiKey,
12
+ getApiEndpoint,
13
+ setApiEndpoint,
14
+ getTicketSystem,
15
+ setTicketSystem,
16
+ } from './config.js';
10
17
  import { formatReviewOutput } from './formatter.js';
11
18
 
12
19
  /**
@@ -30,8 +37,10 @@ async function confirmAction(message) {
30
37
  program
31
38
  .name('kk')
32
39
  .description('AI-powered code review CLI - Keep your kode korekt')
33
- .version('1.0.0')
34
- .addHelpText('after', `
40
+ .version('0.2.0')
41
+ .addHelpText(
42
+ 'after',
43
+ `
35
44
  Examples:
36
45
  $ kk review Review committed changes (auto-detect base)
37
46
  $ kk review main Review changes against main branch
@@ -47,24 +56,29 @@ Configuration:
47
56
  $ kk config --key YOUR_KEY
48
57
  $ kk config --endpoint https://api.korekt.ai/review/local
49
58
  $ kk config --ticket-system ado
50
- `);
59
+ `
60
+ );
51
61
 
52
62
  program
53
63
  .command('review')
54
64
  .description('Review the changes in the current branch.')
55
- .argument('[target-branch]', 'The branch to compare against (e.g., main, develop). If not specified, auto-detects fork point.')
65
+ .argument(
66
+ '[target-branch]',
67
+ 'The branch to compare against (e.g., main, develop). If not specified, auto-detects fork point.'
68
+ )
56
69
  .option('--ticket-system <system>', 'Ticket system to use (jira or ado)')
57
70
  .option('--dry-run', 'Show payload without sending to API')
58
- .option('--ignore <patterns...>', 'Ignore files matching these patterns (e.g., "*.lock" "dist/*")')
71
+ .option(
72
+ '--ignore <patterns...>',
73
+ 'Ignore files matching these patterns (e.g., "*.lock" "dist/*")'
74
+ )
59
75
  .action(async (targetBranch, options) => {
60
76
  const reviewTarget = targetBranch ? `against '${targetBranch}'` : '(auto-detecting fork point)';
61
77
  console.log(chalk.blue.bold(`🚀 Starting AI Code Review ${reviewTarget}...`));
62
78
 
63
79
  const apiKey = getApiKey();
64
80
  if (!apiKey) {
65
- console.error(
66
- chalk.red('API Key not found! Please run `kk config --key YOUR_KEY` first.')
67
- );
81
+ console.error(chalk.red('API Key not found! Please run `kk config --key YOUR_KEY` first.'));
68
82
  return;
69
83
  }
70
84
 
@@ -107,12 +121,18 @@ program
107
121
  // Create a shortened version for display
108
122
  const displayPayload = {
109
123
  ...payload,
110
- changed_files: payload.changed_files.map(file => ({
124
+ changed_files: payload.changed_files.map((file) => ({
111
125
  path: file.path,
112
126
  status: file.status,
113
127
  ...(file.old_path && { old_path: file.old_path }),
114
- diff: file.diff.length > 500 ? `${file.diff.substring(0, 500)}... [truncated ${file.diff.length - 500} chars]` : file.diff,
115
- content: file.content.length > 500 ? `${file.content.substring(0, 500)}... [truncated ${file.content.length - 500} chars]` : file.content,
128
+ diff:
129
+ file.diff.length > 500
130
+ ? `${file.diff.substring(0, 500)}... [truncated ${file.diff.length - 500} chars]`
131
+ : file.diff,
132
+ content:
133
+ file.content.length > 500
134
+ ? `${file.content.substring(0, 500)}... [truncated ${file.content.length - 500} chars]`
135
+ : file.content,
116
136
  })),
117
137
  };
118
138
 
@@ -129,14 +149,15 @@ program
129
149
  console.log(` Files: ${chalk.cyan(payload.changed_files.length)}\n`);
130
150
 
131
151
  console.log(chalk.bold(' Files to review:'));
132
- payload.changed_files.forEach(file => {
133
- const statusColor = {
134
- 'M': chalk.yellow,
135
- 'A': chalk.green,
136
- 'D': chalk.red,
137
- 'R': chalk.blue,
138
- 'C': chalk.cyan,
139
- }[file.status] || (text => text);
152
+ payload.changed_files.forEach((file) => {
153
+ const statusColor =
154
+ {
155
+ M: chalk.yellow,
156
+ A: chalk.green,
157
+ D: chalk.red,
158
+ R: chalk.blue,
159
+ C: chalk.cyan,
160
+ }[file.status] || ((text) => text);
140
161
  console.log(` ${statusColor(file.status + ' ' + file.path)}`);
141
162
  });
142
163
  console.log();
@@ -161,7 +182,7 @@ program
161
182
  try {
162
183
  const response = await axios.post(apiEndpoint, payload, {
163
184
  headers: {
164
- 'Authorization': `Bearer ${apiKey}`,
185
+ Authorization: `Bearer ${apiKey}`,
165
186
  'Content-Type': 'application/json',
166
187
  },
167
188
  });
@@ -224,9 +245,7 @@ program
224
245
  async function reviewUncommitted(mode, options) {
225
246
  const apiKey = getApiKey();
226
247
  if (!apiKey) {
227
- console.error(
228
- chalk.red('API Key not found! Please run `kk config --key YOUR_KEY` first.')
229
- );
248
+ console.error(chalk.red('API Key not found! Please run `kk config --key YOUR_KEY` first.'));
230
249
  return;
231
250
  }
232
251
 
@@ -265,12 +284,18 @@ async function reviewUncommitted(mode, options) {
265
284
 
266
285
  const displayPayload = {
267
286
  ...payload,
268
- changed_files: payload.changed_files.map(file => ({
287
+ changed_files: payload.changed_files.map((file) => ({
269
288
  path: file.path,
270
289
  status: file.status,
271
290
  ...(file.old_path && { old_path: file.old_path }),
272
- diff: file.diff.length > 500 ? `${file.diff.substring(0, 500)}... [truncated ${file.diff.length - 500} chars]` : file.diff,
273
- content: file.content.length > 500 ? `${file.content.substring(0, 500)}... [truncated ${file.content.length - 500} chars]` : file.content,
291
+ diff:
292
+ file.diff.length > 500
293
+ ? `${file.diff.substring(0, 500)}... [truncated ${file.diff.length - 500} chars]`
294
+ : file.diff,
295
+ content:
296
+ file.content.length > 500
297
+ ? `${file.content.substring(0, 500)}... [truncated ${file.content.length - 500} chars]`
298
+ : file.content,
274
299
  })),
275
300
  };
276
301
 
@@ -284,14 +309,15 @@ async function reviewUncommitted(mode, options) {
284
309
  console.log(chalk.yellow('\n📋 Ready to submit uncommitted changes for review:\n'));
285
310
  console.log(chalk.gray(' Comparing against HEAD (last commit)\n'));
286
311
  console.log(chalk.bold(' Files to review:'));
287
- payload.changed_files.forEach(file => {
288
- const statusColor = {
289
- 'M': chalk.yellow,
290
- 'A': chalk.green,
291
- 'D': chalk.red,
292
- 'R': chalk.blue,
293
- 'C': chalk.cyan,
294
- }[file.status] || (text => text);
312
+ payload.changed_files.forEach((file) => {
313
+ const statusColor =
314
+ {
315
+ M: chalk.yellow,
316
+ A: chalk.green,
317
+ D: chalk.red,
318
+ R: chalk.blue,
319
+ C: chalk.cyan,
320
+ }[file.status] || ((text) => text);
295
321
  console.log(` ${statusColor(file.status + ' ' + file.path)}`);
296
322
  });
297
323
  console.log();
@@ -315,7 +341,7 @@ async function reviewUncommitted(mode, options) {
315
341
  try {
316
342
  const response = await axios.post(apiEndpoint, payload, {
317
343
  headers: {
318
- 'Authorization': `Bearer ${apiKey}`,
344
+ Authorization: `Bearer ${apiKey}`,
319
345
  'Content-Type': 'application/json',
320
346
  },
321
347
  });
@@ -355,8 +381,12 @@ program
355
381
 
356
382
  console.log(chalk.bold('\nCurrent Configuration:\n'));
357
383
  console.log(` API Key: ${apiKey ? chalk.green('✓ Set') : chalk.red('✗ Not set')}`);
358
- console.log(` API Endpoint: ${apiEndpoint ? chalk.cyan(apiEndpoint) : chalk.red('✗ Not set')}`);
359
- console.log(` Ticket System: ${ticketSystem ? chalk.cyan(ticketSystem) : chalk.gray('Not configured')}\n`);
384
+ console.log(
385
+ ` API Endpoint: ${apiEndpoint ? chalk.cyan(apiEndpoint) : chalk.red('Not set')}`
386
+ );
387
+ console.log(
388
+ ` Ticket System: ${ticketSystem ? chalk.cyan(ticketSystem) : chalk.gray('Not configured')}\n`
389
+ );
360
390
  return;
361
391
  }
362
392