korekt-cli 0.3.0 → 0.4.1

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 CHANGED
@@ -4,7 +4,7 @@
4
4
  [![npm downloads](https://img.shields.io/npm/dm/korekt-cli.svg)](https://www.npmjs.com/package/korekt-cli)
5
5
  [![license](https://img.shields.io/npm/l/korekt-cli.svg)](https://www.npmjs.com/package/korekt-cli)
6
6
 
7
- AI-powered code review CLI - Keep your kode korekt
7
+ AI-powered code review CLI - Keep your kode korekt
8
8
 
9
9
  `kk` integrates seamlessly with your local Git workflow to provide intelligent code reviews powered by AI.
10
10
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "korekt-cli",
3
- "version": "0.3.0",
3
+ "version": "0.4.1",
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",
@@ -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,26 +28,33 @@ 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}`;
49
49
  }
50
50
 
51
+ // Handle Bitbucket SSH format: git@bitbucket.org:user/repo.git
52
+ const bitbucketSshMatch = url.match(/git@bitbucket\.org:([^/]+)\/(.+?)(?:\.git)?$/);
53
+ if (bitbucketSshMatch) {
54
+ const [, user, repo] = bitbucketSshMatch;
55
+ return `https://bitbucket.org/${user}/${repo}`;
56
+ }
57
+
51
58
  // If already HTTPS or other format, return as-is (possibly removing .git suffix)
52
59
  return url.replace(/\.git$/, '');
53
60
  }
@@ -69,11 +76,11 @@ export function shouldIgnoreFile(filePath, patterns) {
69
76
  // Replace * with [^/]* (matches anything except /)
70
77
  // Replace ** with .* (matches anything including /)
71
78
  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 .
79
+ .replace(/\./g, '\\.') // Escape dots
80
+ .replace(/\*\*/g, '___DOUBLESTAR___') // Temporarily replace **
81
+ .replace(/\*/g, '[^/]*') // Replace single * with [^/]*
82
+ .replace(/___DOUBLESTAR___/g, '.*') // Replace ** with .*
83
+ .replace(/\?/g, '.'); // Replace ? with .
77
84
 
78
85
  // Handle leading **/ pattern - make it optional so it matches both with and without directory prefix
79
86
  // For example, **/*.sql should match both "file.sql" and "dir/file.sql"
@@ -133,7 +140,11 @@ export function parseNameStatus(output) {
133
140
  * @param {string|null} ticketSystem - The ticket system to use (jira or ado), or null to skip ticket extraction
134
141
  * @returns {Object|null} - The payload object ready for API submission, or null on error
135
142
  */
136
- export async function runUncommittedReview(mode = 'all', ticketSystem = null, includeUntracked = false) {
143
+ export async function runUncommittedReview(
144
+ mode = 'all',
145
+ _ticketSystem = null,
146
+ includeUntracked = false
147
+ ) {
137
148
  try {
138
149
  // 1. Get Repo URL and current branch name
139
150
  const { stdout: repoUrl } = await execa('git', ['remote', 'get-url', 'origin']);
@@ -164,12 +175,19 @@ export async function runUncommittedReview(mode = 'all', ticketSystem = null, in
164
175
  // Handle untracked files if requested
165
176
  if (includeUntracked) {
166
177
  console.log(chalk.gray('Analyzing untracked files...'));
167
- const { stdout: untrackedFilesOutput } = await execa('git', ['ls-files', '--others', '--exclude-standard']);
178
+ const { stdout: untrackedFilesOutput } = await execa('git', [
179
+ 'ls-files',
180
+ '--others',
181
+ '--exclude-standard',
182
+ ]);
168
183
  const untrackedFiles = untrackedFilesOutput.split('\n').filter(Boolean);
169
184
 
170
185
  for (const file of untrackedFiles) {
171
186
  const content = fs.readFileSync(file, 'utf-8');
172
- const diff = content.split('\n').map(line => `+${line}`).join('\n');
187
+ const diff = content
188
+ .split('\n')
189
+ .map((line) => `+${line}`)
190
+ .join('\n');
173
191
  changedFiles.push({
174
192
  path: file,
175
193
  status: 'A', // Untracked files are always additions
@@ -182,8 +200,8 @@ export async function runUncommittedReview(mode = 'all', ticketSystem = null, in
182
200
  }
183
201
 
184
202
  // 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));
203
+ const processedPaths = new Set(changedFiles.map((f) => f.path));
204
+ const uniqueFileList = fileList.filter((file) => !processedPaths.has(file.path));
187
205
 
188
206
  for (const file of uniqueFileList) {
189
207
  const { status, path, oldPath } = file;
@@ -199,7 +217,13 @@ export async function runUncommittedReview(mode = 'all', ticketSystem = null, in
199
217
  } else {
200
218
  // For 'all', try staged first, then unstaged
201
219
  try {
202
- const { stdout: stagedDiff } = await execa('git', ['diff', '--cached', '-U15', '--', path]);
220
+ const { stdout: stagedDiff } = await execa('git', [
221
+ 'diff',
222
+ '--cached',
223
+ '-U15',
224
+ '--',
225
+ path,
226
+ ]);
203
227
  if (stagedDiff) {
204
228
  diff = stagedDiff;
205
229
  } else {
@@ -218,8 +242,10 @@ export async function runUncommittedReview(mode = 'all', ticketSystem = null, in
218
242
  try {
219
243
  const { stdout: headContent } = await execa('git', ['show', `HEAD:${oldPath}`]);
220
244
  content = headContent;
221
- } catch (e) {
222
- console.warn(chalk.yellow(`Could not get HEAD content for ${oldPath}. Assuming it's new.`));
245
+ } catch {
246
+ console.warn(
247
+ chalk.yellow(`Could not get HEAD content for ${oldPath}. Assuming it's new.`)
248
+ );
223
249
  }
224
250
  }
225
251
 
@@ -268,7 +294,11 @@ export async function runUncommittedReview(mode = 'all', ticketSystem = null, in
268
294
  * @param {string[]|null} ignorePatterns - Array of glob patterns to ignore files
269
295
  * @returns {Object|null} - The payload object ready for API submission, or null on error
270
296
  */
271
- export async function runLocalReview(targetBranch = null, ticketSystem = null, ignorePatterns = null) {
297
+ export async function runLocalReview(
298
+ targetBranch = null,
299
+ _ticketSystem = null,
300
+ ignorePatterns = null
301
+ ) {
272
302
  try {
273
303
  // 1. Get Repo URL, current branch name, and repository root
274
304
  const { stdout: repoUrl } = await execa('git', ['remote', 'get-url', 'origin']);
@@ -285,7 +315,7 @@ export async function runLocalReview(targetBranch = null, ticketSystem = null, i
285
315
  // Check if the branch exists locally
286
316
  try {
287
317
  await execa('git', ['rev-parse', '--verify', targetBranch]);
288
- } catch (error) {
318
+ } catch {
289
319
  console.error(chalk.red(`Branch '${targetBranch}' does not exist locally.`));
290
320
  console.error(chalk.gray(`Please check out the branch first or specify a different one.`));
291
321
  return null;
@@ -299,8 +329,10 @@ export async function runLocalReview(targetBranch = null, ticketSystem = null, i
299
329
  // If fetch succeeded, use the remote-tracking branch for comparison
300
330
  // This is safer as it doesn't modify the user's local branch
301
331
  targetBranchRef = `origin/${targetBranch}`;
302
- console.log(chalk.gray(`Using remote-tracking branch 'origin/${targetBranch}' for comparison.`));
303
- } catch (fetchError) {
332
+ console.log(
333
+ chalk.gray(`Using remote-tracking branch 'origin/${targetBranch}' for comparison.`)
334
+ );
335
+ } catch {
304
336
  console.warn(chalk.yellow(`Could not fetch remote branch 'origin/${targetBranch}'.`));
305
337
  console.warn(chalk.gray(`Proceeding with local branch '${targetBranch}' for comparison.`));
306
338
  // targetBranchRef stays as targetBranch (local branch)
@@ -313,7 +345,12 @@ export async function runLocalReview(targetBranch = null, ticketSystem = null, i
313
345
  if (!targetBranch) {
314
346
  try {
315
347
  // Use git reflog to find where the branch was created
316
- const { stdout: reflog } = await execa('git', ['reflog', 'show', '--no-abbrev-commit', branchName]);
348
+ const { stdout: reflog } = await execa('git', [
349
+ 'reflog',
350
+ 'show',
351
+ '--no-abbrev-commit',
352
+ branchName,
353
+ ]);
317
354
  const lines = reflog.split('\n');
318
355
 
319
356
  // Look for the branch creation point (last line in reflog)
@@ -322,15 +359,19 @@ export async function runLocalReview(targetBranch = null, ticketSystem = null, i
322
359
  const match = creationLine.match(/^([a-f0-9]{40})/);
323
360
  if (match) {
324
361
  mergeBase = match[1];
325
- console.log(chalk.gray(`Auto-detected fork point from reflog: ${mergeBase.substring(0, 7)}`));
362
+ console.log(
363
+ chalk.gray(`Auto-detected fork point from reflog: ${mergeBase.substring(0, 7)}`)
364
+ );
326
365
  }
327
366
  }
328
367
 
329
368
  if (!mergeBase) {
330
369
  throw new Error('Could not find fork point in reflog');
331
370
  }
332
- } catch (error) {
333
- console.error(chalk.red('Could not auto-detect fork point. Please specify a target branch.'));
371
+ } catch {
372
+ console.error(
373
+ chalk.red('Could not auto-detect fork point. Please specify a target branch.')
374
+ );
334
375
  console.error(chalk.gray('Usage: kk review <target-branch>'));
335
376
  return null;
336
377
  }
@@ -349,21 +390,25 @@ export async function runLocalReview(targetBranch = null, ticketSystem = null, i
349
390
  console.log(chalk.gray(`Analyzing commits from ${mergeBase.substring(0, 7)} to HEAD...`));
350
391
 
351
392
  // 3. Get Commit Messages with proper delimiter
352
- const { stdout: logOutput } = await execa('git', ['log', '--pretty=%B---EOC---', diffRange], { cwd: repoRootPath });
393
+ const { stdout: logOutput } = await execa('git', ['log', '--pretty=%B---EOC---', diffRange], {
394
+ cwd: repoRootPath,
395
+ });
353
396
  const commitMessages = logOutput
354
397
  .split('---EOC---')
355
398
  .map((msg) => msg.trim())
356
399
  .filter(Boolean);
357
400
 
358
401
  // 4. Get changed files and their status
359
- const { stdout: nameStatusOutput } = await execa('git', ['diff', '--name-status', diffRange], { cwd: repoRootPath });
402
+ const { stdout: nameStatusOutput } = await execa('git', ['diff', '--name-status', diffRange], {
403
+ cwd: repoRootPath,
404
+ });
360
405
  const fileList = parseNameStatus(nameStatusOutput);
361
406
 
362
407
  // Filter out ignored files
363
408
  let filteredFileList = fileList;
364
409
  let ignoredCount = 0;
365
410
  if (ignorePatterns && ignorePatterns.length > 0) {
366
- filteredFileList = fileList.filter(file => {
411
+ filteredFileList = fileList.filter((file) => {
367
412
  const ignored = shouldIgnoreFile(file.path, ignorePatterns);
368
413
  if (ignored) {
369
414
  ignoredCount++;
@@ -385,19 +430,22 @@ export async function runLocalReview(targetBranch = null, ticketSystem = null, i
385
430
 
386
431
  // Run git commands from the repository root to handle all file paths correctly
387
432
  // 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 });
433
+ const { stdout: diff } = await execa('git', ['diff', '-U15', diffRange, '--', path], {
434
+ cwd: repoRootPath,
435
+ });
389
436
 
390
437
  // Get the original content from the base commit
391
438
  let content = '';
392
439
  if (status !== 'A') {
393
440
  // Added files have no original content
394
441
  try {
395
- const { stdout: originalContent } = await execa('git', [
396
- 'show',
397
- `${mergeBase.trim()}:${oldPath}`,
398
- ], { cwd: repoRootPath });
442
+ const { stdout: originalContent } = await execa(
443
+ 'git',
444
+ ['show', `${mergeBase.trim()}:${oldPath}`],
445
+ { cwd: repoRootPath }
446
+ );
399
447
  content = originalContent;
400
- } catch (e) {
448
+ } catch {
401
449
  // This can happen if a file was added and modified in the same branch
402
450
  console.warn(
403
451
  chalk.yellow(`Could not get original content for ${oldPath}. Assuming it was added.`)
@@ -437,4 +485,4 @@ export async function runLocalReview(targetBranch = null, ticketSystem = null, i
437
485
  }
438
486
  return null; // Return null to indicate failure
439
487
  }
440
- }
488
+ }
@@ -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
  });
@@ -450,6 +457,18 @@ describe('normalizeRepoUrl', () => {
450
457
  expect(normalizeRepoUrl(sshUrl)).toBe(expected);
451
458
  });
452
459
 
460
+ it('should normalize Bitbucket SSH URL to HTTPS', () => {
461
+ const sshUrl = 'git@bitbucket.org:user/repo.git';
462
+ const expected = 'https://bitbucket.org/user/repo';
463
+ expect(normalizeRepoUrl(sshUrl)).toBe(expected);
464
+ });
465
+
466
+ it('should normalize Bitbucket SSH URL without .git suffix', () => {
467
+ const sshUrl = 'git@bitbucket.org:user/repo';
468
+ const expected = 'https://bitbucket.org/user/repo';
469
+ expect(normalizeRepoUrl(sshUrl)).toBe(expected);
470
+ });
471
+
453
472
  it('should remove .git suffix from HTTPS URLs', () => {
454
473
  const httpsUrl = 'https://github.com/user/repo.git';
455
474
  const expected = 'https://github.com/user/repo';
@@ -539,4 +558,4 @@ describe('shouldIgnoreFile', () => {
539
558
  expect(shouldIgnoreFile('file.sql', pattern)).toBe(true);
540
559
  expect(shouldIgnoreFile('file.js', pattern)).toBe(false);
541
560
  });
542
- });
561
+ });
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
  /**
@@ -31,7 +38,9 @@ program
31
38
  .name('kk')
32
39
  .description('AI-powered code review CLI - Keep your kode korekt')
33
40
  .version('0.2.0')
34
- .addHelpText('after', `
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