korekt-cli 0.6.0 → 0.8.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 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 System Integration**: Automatically extracts ticket IDs from branch names and commit messages (Jira & Azure DevOps)
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/local
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
- # Review all uncommitted changes (staged + unstaged)
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/local
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/local"
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "korekt-cli",
3
- "version": "0.6.0",
3
+ "version": "0.8.0",
4
4
  "description": "AI-powered code review CLI - Keep your kode korekt",
5
5
  "main": "src/index.js",
6
6
  "bin": {
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/local';
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, unstaged, or all)
140
- * @param {string} mode - 'staged', 'unstaged', or 'all'
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 if (mode === 'unstaged') {
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
- // Handle untracked files if requested
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
- // For 'all', try staged first, then unstaged
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)
@@ -332,15 +281,10 @@ export async function getContributors(diffRange, repoRootPath) {
332
281
  /**
333
282
  * Main function to analyze local git changes and prepare review payload
334
283
  * @param {string|null} targetBranch - The branch to compare against. If null, uses git reflog to find fork point.
335
- * @param {string|null} ticketSystem - The ticket system to use (jira or ado), or null to skip ticket extraction
336
284
  * @param {string[]|null} ignorePatterns - Array of glob patterns to ignore files
337
285
  * @returns {Object|null} - The payload object ready for API submission, or null on error
338
286
  */
339
- export async function runLocalReview(
340
- targetBranch = null,
341
- _ticketSystem = null,
342
- ignorePatterns = null
343
- ) {
287
+ export async function runLocalReview(targetBranch = null, ignorePatterns = null) {
344
288
  try {
345
289
  // 1. Get Repo URL, current branch name, and repository root
346
290
  const { stdout: repoUrl } = await execa('git', ['remote', 'get-url', 'origin']);
@@ -83,7 +83,7 @@ describe('runUncommittedReview', () => {
83
83
  throw new Error(`Unmocked command: ${command}`);
84
84
  });
85
85
 
86
- const result = await runUncommittedReview('staged', null);
86
+ const result = await runUncommittedReview('staged');
87
87
 
88
88
  expect(result).toBeDefined();
89
89
  expect(result.repo_url).toBe('https://github.com/user/repo'); // Normalized (no .git)
@@ -120,56 +120,13 @@ describe('runUncommittedReview', () => {
120
120
  throw new Error(`Unmocked command: ${command}`);
121
121
  });
122
122
 
123
- const result = await runUncommittedReview('unstaged', null);
123
+ const result = await runUncommittedReview('unstaged');
124
124
 
125
125
  expect(result).toBeDefined();
126
126
  expect(result.source_branch).toBe('feature-branch');
127
127
  expect(result.changed_files).toHaveLength(1);
128
128
  });
129
129
 
130
- it('should analyze all uncommitted changes', async () => {
131
- vi.mocked(execa).mockImplementation(async (cmd, args) => {
132
- const command = [cmd, ...args].join(' ');
133
-
134
- if (command.includes('remote get-url origin')) {
135
- return { stdout: 'https://github.com/user/repo.git' };
136
- }
137
- if (command.includes('rev-parse --abbrev-ref HEAD')) {
138
- return { stdout: 'feature-branch' };
139
- }
140
- if (command.includes('rev-parse --show-toplevel')) {
141
- return { stdout: '/fake/repo/path' };
142
- }
143
- if (command.includes('diff --cached --name-status')) {
144
- return { stdout: 'M\tstaged.js' };
145
- }
146
- if (command === 'git diff --name-status') {
147
- return { stdout: 'M\tunstaged.js' };
148
- }
149
- if (command.includes('diff --cached -U15 -- staged.js')) {
150
- return { stdout: 'diff staged' };
151
- }
152
- if (command.includes('diff -U15 -- unstaged.js')) {
153
- return { stdout: 'diff unstaged' };
154
- }
155
- if (command.includes('show HEAD:staged.js')) {
156
- return { stdout: 'staged old content' };
157
- }
158
- if (command.includes('show HEAD:unstaged.js')) {
159
- return { stdout: 'unstaged old content' };
160
- }
161
-
162
- throw new Error(`Unmocked command: ${command}`);
163
- });
164
-
165
- const result = await runUncommittedReview('all', null);
166
-
167
- expect(result).toBeDefined();
168
- expect(result.changed_files).toHaveLength(2);
169
- expect(result.changed_files[0].path).toBe('staged.js');
170
- expect(result.changed_files[1].path).toBe('unstaged.js');
171
- });
172
-
173
130
  it('should return null when no changes found', async () => {
174
131
  vi.mocked(execa).mockImplementation(async (cmd, args) => {
175
132
  const command = [cmd, ...args].join(' ');
@@ -193,7 +150,7 @@ describe('runUncommittedReview', () => {
193
150
  throw new Error(`Unmocked command: ${command}`);
194
151
  });
195
152
 
196
- const result = await runUncommittedReview('all', null);
153
+ const result = await runUncommittedReview('staged');
197
154
 
198
155
  expect(result).toBeNull();
199
156
  });
@@ -347,7 +304,7 @@ describe('runLocalReview - fork point detection', () => {
347
304
  throw new Error(`Unmocked command: ${command}`);
348
305
  });
349
306
 
350
- const result = await runLocalReview(null, 'jira');
307
+ const result = await runLocalReview(null);
351
308
 
352
309
  expect(result).toBeDefined();
353
310
  expect(result.source_branch).toBe('feature-branch');
@@ -397,7 +354,7 @@ describe('runLocalReview - fork point detection', () => {
397
354
  throw new Error(`Unmocked command: ${command}`);
398
355
  });
399
356
 
400
- const result = await runLocalReview('main', 'jira');
357
+ const result = await runLocalReview('main');
401
358
 
402
359
  expect(result).toBeDefined();
403
360
 
package/src/index.js CHANGED
@@ -6,18 +6,13 @@ import chalk from 'chalk';
6
6
  import readline from 'readline';
7
7
  import ora from 'ora';
8
8
  import { createRequire } from 'module';
9
- import { readFileSync } from 'fs';
10
- import { join, dirname } from 'path';
9
+ import { writeFileSync, mkdtempSync, rmSync } from 'fs';
10
+ import { join, dirname, resolve } from 'path';
11
11
  import { fileURLToPath } from 'url';
12
+ import { spawn } from 'child_process';
13
+ import { tmpdir } from 'os';
12
14
  import { runLocalReview } from './git-logic.js';
13
- import {
14
- getApiKey,
15
- setApiKey,
16
- getApiEndpoint,
17
- setApiEndpoint,
18
- getTicketSystem,
19
- setTicketSystem,
20
- } from './config.js';
15
+ import { getApiKey, setApiKey, getApiEndpoint, setApiEndpoint } from './config.js';
21
16
  import { formatReviewOutput } from './formatter.js';
22
17
 
23
18
  const require = createRequire(import.meta.url);
@@ -78,11 +73,76 @@ async function confirmAction(message) {
78
73
  output: process.stdout,
79
74
  });
80
75
 
81
- return new Promise((resolve) => {
76
+ return new Promise((resolvePromise) => {
82
77
  rl.question(message, (answer) => {
83
78
  rl.close();
84
79
  // Default to 'yes' if the user just presses Enter
85
- resolve(answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes' || answer === '');
80
+ resolvePromise(
81
+ answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes' || answer === ''
82
+ );
83
+ });
84
+ });
85
+ }
86
+
87
+ /**
88
+ * Detect CI provider from environment variables
89
+ * @returns {string|null} Provider name or null if not detected
90
+ */
91
+ export function detectCIProvider() {
92
+ if (process.env.GITHUB_TOKEN && process.env.GITHUB_REPOSITORY) {
93
+ return 'github';
94
+ }
95
+ if (process.env.SYSTEM_ACCESSTOKEN && process.env.SYSTEM_PULLREQUEST_PULLREQUESTID) {
96
+ return 'azure';
97
+ }
98
+ if (process.env.BITBUCKET_REPO_SLUG && process.env.BITBUCKET_PR_ID) {
99
+ return 'bitbucket';
100
+ }
101
+ return null;
102
+ }
103
+
104
+ /**
105
+ * Run the CI integration script to post comments
106
+ * @param {string} provider - CI provider (github, azure, bitbucket)
107
+ * @param {Object} results - Review results from API
108
+ * @returns {Promise<void>}
109
+ */
110
+ async function runCIScript(provider, results) {
111
+ const __filename = fileURLToPath(import.meta.url);
112
+ const __dirname = dirname(__filename);
113
+ const scriptPath = resolve(__dirname, '..', 'scripts', `${provider}.sh`);
114
+
115
+ // Create secure temp directory and write results
116
+ const tempDir = mkdtempSync(join(tmpdir(), 'korekt-'));
117
+ const tempFile = join(tempDir, 'results.json');
118
+ writeFileSync(tempFile, JSON.stringify(results, null, 2));
119
+
120
+ return new Promise((resolvePromise, reject) => {
121
+ const cleanup = () => {
122
+ try {
123
+ rmSync(tempDir, { recursive: true, force: true });
124
+ } catch (err) {
125
+ log(chalk.yellow(`Warning: Failed to clean up temp directory: ${err.message}`));
126
+ }
127
+ };
128
+
129
+ const child = spawn('bash', [scriptPath, tempFile], {
130
+ stdio: 'inherit',
131
+ env: process.env,
132
+ });
133
+
134
+ child.on('close', (code) => {
135
+ cleanup();
136
+ if (code === 0) {
137
+ resolvePromise();
138
+ } else {
139
+ reject(new Error(`CI script exited with code ${code}`));
140
+ }
141
+ });
142
+
143
+ child.on('error', (err) => {
144
+ cleanup();
145
+ reject(err);
86
146
  });
87
147
  });
88
148
  }
@@ -99,23 +159,17 @@ Examples:
99
159
  $ kk review main Review changes against main branch
100
160
  $ kk stg --dry-run Preview staged changes review
101
161
  $ kk diff Review unstaged changes
102
- $ kk all Review all uncommitted changes
103
162
  $ kk review main --json Output raw JSON (for CI/CD integration)
163
+ $ kk review main --comment Review and post comments to PR (CI/CD)
104
164
 
105
165
  Common Options:
106
166
  --dry-run Show payload without sending to API
107
167
  --json Output raw API response as JSON
108
- --ticket-system <system> Use specific ticket system (jira or ado)
168
+ --comment Post review results as PR comments
109
169
 
110
170
  Configuration:
111
171
  $ kk config --key YOUR_KEY
112
- $ kk config --endpoint https://api.korekt.ai/review/local
113
- $ kk config --ticket-system ado
114
-
115
- CI/CD Integration:
116
- $ kk get-script github Output GitHub Actions integration script
117
- $ kk get-script bitbucket Output Bitbucket Pipelines integration script
118
- $ kk get-script azure Output Azure DevOps integration script
172
+ $ kk config --endpoint https://api.korekt.ai/api/review
119
173
  `
120
174
  );
121
175
 
@@ -126,13 +180,13 @@ program
126
180
  '[target-branch]',
127
181
  'The branch to compare against (e.g., main, develop). If not specified, auto-detects fork point.'
128
182
  )
129
- .option('--ticket-system <system>', 'Ticket system to use (jira or ado)')
130
183
  .option('--dry-run', 'Show payload without sending to API')
131
184
  .option(
132
185
  '--ignore <patterns...>',
133
186
  'Ignore files matching these patterns (e.g., "*.lock" "dist/*")'
134
187
  )
135
188
  .option('--json', 'Output raw API response as JSON')
189
+ .option('--comment', 'Post review results as PR comments (auto-detects CI provider)')
136
190
  .action(async (targetBranch, options) => {
137
191
  const reviewTarget = targetBranch ? `against '${targetBranch}'` : '(auto-detecting fork point)';
138
192
 
@@ -153,30 +207,14 @@ program
153
207
  process.exit(1);
154
208
  }
155
209
 
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
210
  // Gather all data using our git logic module
167
- const payload = await runLocalReview(targetBranch, ticketSystem, options.ignore);
211
+ const payload = await runLocalReview(targetBranch, options.ignore);
168
212
 
169
213
  if (!payload) {
170
214
  log(chalk.red('Could not proceed with review due to errors during analysis.'));
171
215
  process.exit(1);
172
216
  }
173
217
 
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
218
  // If dry-run, just show the payload and exit
181
219
  if (options.dryRun) {
182
220
  log(chalk.yellow('\n📋 Dry Run - Payload that would be sent:\n'));
@@ -193,8 +231,8 @@ program
193
231
  return;
194
232
  }
195
233
 
196
- // Show summary and ask for confirmation (auto-confirm in JSON mode)
197
- if (!options.json) {
234
+ // Show summary and ask for confirmation (auto-confirm in JSON/comment mode)
235
+ if (!options.json && !options.comment) {
198
236
  log(chalk.yellow('\n📋 Ready to submit for review:\n'));
199
237
  log(` Branch: ${chalk.cyan(payload.source_branch)}`);
200
238
  log(` Commits: ${chalk.cyan(payload.commit_messages.length)}`);
@@ -243,6 +281,32 @@ program
243
281
  const elapsed = Math.floor((Date.now() - startTime) / 1000);
244
282
  spinner.succeed(`Review completed in ${elapsed}s!`);
245
283
 
284
+ // Handle --comment flag: post results to PR
285
+ if (options.comment) {
286
+ const provider = detectCIProvider();
287
+ if (!provider) {
288
+ log(
289
+ chalk.red(
290
+ 'Could not detect CI provider. Make sure required environment variables are set:'
291
+ )
292
+ );
293
+ log(chalk.gray(' GitHub: GITHUB_TOKEN, GITHUB_REPOSITORY, PR_NUMBER, COMMIT_HASH'));
294
+ log(chalk.gray(' Azure: SYSTEM_ACCESSTOKEN, SYSTEM_PULLREQUEST_PULLREQUESTID'));
295
+ log(chalk.gray(' Bitbucket: BITBUCKET_REPO_SLUG, BITBUCKET_PR_ID'));
296
+ process.exit(1);
297
+ }
298
+
299
+ log(chalk.blue(`Posting review comments to ${provider}...`));
300
+ try {
301
+ await runCIScript(provider, response.data);
302
+ log(chalk.green('Successfully posted review comments!'));
303
+ } catch (err) {
304
+ log(chalk.red(`Failed to post comments: ${err.message}`));
305
+ process.exit(1);
306
+ }
307
+ return;
308
+ }
309
+
246
310
  // Output results to stdout
247
311
  if (options.json) {
248
312
  output(JSON.stringify(response.data, null, 2));
@@ -276,7 +340,6 @@ program
276
340
  .command('review-staged')
277
341
  .aliases(['stg', 'staged', 'cached'])
278
342
  .description('Review staged changes (git diff --cached)')
279
- .option('--ticket-system <system>', 'Ticket system to use (jira or ado)')
280
343
  .option('--dry-run', 'Show payload without sending to API')
281
344
  .option('--json', 'Output raw API response as JSON')
282
345
  .action(async (options) => {
@@ -288,28 +351,13 @@ program
288
351
  .command('review-unstaged')
289
352
  .alias('diff')
290
353
  .description('Review unstaged changes (git diff)')
291
- .option('--ticket-system <system>', 'Ticket system to use (jira or ado)')
292
354
  .option('--dry-run', 'Show payload without sending to API')
293
- .option('--untracked', 'Include untracked files in the review')
294
355
  .option('--json', 'Output raw API response as JSON')
295
356
  .action(async (options) => {
296
357
  log(chalk.blue.bold('🚀 Reviewing unstaged changes...'));
297
358
  await reviewUncommitted('unstaged', options);
298
359
  });
299
360
 
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
361
  async function reviewUncommitted(mode, options) {
314
362
  const apiKey = getApiKey();
315
363
  if (!apiKey) {
@@ -325,27 +373,14 @@ async function reviewUncommitted(mode, options) {
325
373
  process.exit(1);
326
374
  }
327
375
 
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
376
  const { runUncommittedReview } = await import('./git-logic.js');
337
- const payload = await runUncommittedReview(mode, ticketSystem, options.untracked);
377
+ const payload = await runUncommittedReview(mode);
338
378
 
339
379
  if (!payload) {
340
380
  log(chalk.red('No changes found or error occurred during analysis.'));
341
381
  process.exit(1);
342
382
  }
343
383
 
344
- if (ticketSystem) {
345
- payload.ticket_system = ticketSystem;
346
- log(chalk.gray(`Using ticket system: ${ticketSystem}`));
347
- }
348
-
349
384
  if (options.dryRun) {
350
385
  log(chalk.yellow('\n📋 Dry Run - Payload that would be sent:\n'));
351
386
 
@@ -440,22 +475,17 @@ program
440
475
  .description('Configure API settings')
441
476
  .option('--key <key>', 'Your API key')
442
477
  .option('--endpoint <endpoint>', 'Your API endpoint URL')
443
- .option('--ticket-system <system>', 'Ticket system (jira, ado)')
444
478
  .option('--show', 'Show current configuration')
445
479
  .action((options) => {
446
480
  // Show current config if --show flag is used
447
481
  if (options.show) {
448
482
  const apiKey = getApiKey();
449
483
  const apiEndpoint = getApiEndpoint();
450
- const ticketSystem = getTicketSystem();
451
484
 
452
485
  console.log(chalk.bold('\nCurrent Configuration:\n'));
453
486
  console.log(` API Key: ${apiKey ? chalk.green('✓ Set') : chalk.red('✗ Not set')}`);
454
487
  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`
488
+ ` API Endpoint: ${apiEndpoint ? chalk.cyan(apiEndpoint) : chalk.red('✗ Not set')}\n`
459
489
  );
460
490
  return;
461
491
  }
@@ -476,76 +506,15 @@ program
476
506
  setApiEndpoint(options.endpoint);
477
507
  console.log(chalk.green('✓ API Endpoint saved successfully!'));
478
508
  }
479
- if (options.ticketSystem !== undefined) {
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) {
509
+ if (!options.key && !options.endpoint && !options.show) {
497
510
  console.log(chalk.yellow('Please provide at least one configuration option.'));
498
511
  console.log('\nUsage:');
499
512
  console.log(' kk config --key YOUR_API_KEY');
500
- console.log(' kk config --endpoint https://api.korekt.ai/review/local');
501
- console.log(' kk config --ticket-system jira');
513
+ console.log(' kk config --endpoint https://api.korekt.ai/api/review');
502
514
  console.log(' kk config --show (view current configuration)');
503
515
  }
504
516
  });
505
517
 
506
- program
507
- .command('get-script <provider>')
508
- .description('Output a CI/CD integration script for a specific provider')
509
- .addHelpText(
510
- 'after',
511
- `
512
- Providers:
513
- github GitHub Actions integration script
514
- bitbucket Bitbucket Pipelines integration script
515
- azure Azure DevOps integration script
516
-
517
- Usage:
518
- kk get-script github | bash -s results.json
519
- kk get-script bitbucket > bitbucket.sh && chmod +x bitbucket.sh
520
- kk get-script azure > azure.sh
521
- `
522
- )
523
- .action((provider) => {
524
- const validProviders = ['github', 'bitbucket', 'azure'];
525
-
526
- if (!validProviders.includes(provider.toLowerCase())) {
527
- console.error(chalk.red(`Invalid provider: ${provider}`));
528
- console.error(chalk.gray(`Valid providers: ${validProviders.join(', ')}`));
529
- process.exit(1);
530
- }
531
-
532
- try {
533
- // Get the directory where this script is located
534
- const __filename = fileURLToPath(import.meta.url);
535
- const __dirname = dirname(__filename);
536
-
537
- // Build path to the script file
538
- const scriptPath = join(__dirname, '..', 'scripts', `${provider.toLowerCase()}.sh`);
539
-
540
- // Read and output the script
541
- const scriptContent = readFileSync(scriptPath, 'utf8');
542
- output(scriptContent);
543
- } catch (error) {
544
- console.error(chalk.red(`Failed to read script: ${error.message}`));
545
- process.exit(1);
546
- }
547
- });
548
-
549
518
  // Only parse arguments if this file is being run directly (not imported)
550
519
  // In tests, we set NODE_ENV to 'test' via vitest
551
520
  if (process.env.NODE_ENV !== 'test') {
package/src/index.test.js CHANGED
@@ -1,8 +1,5 @@
1
1
  import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
- import { truncateFileData, formatErrorOutput } from './index.js';
3
- import { readFileSync } from 'fs';
4
- import { join, dirname } from 'path';
5
- import { fileURLToPath } from 'url';
2
+ import { truncateFileData, formatErrorOutput, detectCIProvider } from './index.js';
6
3
 
7
4
  describe('CLI JSON output mode', () => {
8
5
  let stdoutSpy;
@@ -336,137 +333,108 @@ describe('CLI JSON output mode', () => {
336
333
  });
337
334
  });
338
335
 
339
- describe('get-script command', () => {
340
- let stdoutSpy;
336
+ describe('detectCIProvider', () => {
337
+ const originalEnv = process.env;
341
338
 
342
339
  beforeEach(() => {
343
- stdoutSpy = vi.spyOn(process.stdout, 'write').mockImplementation(() => true);
340
+ // Reset environment before each test
341
+ process.env = { ...originalEnv };
342
+ // Clear all CI-related env vars
343
+ delete process.env.GITHUB_TOKEN;
344
+ delete process.env.GITHUB_REPOSITORY;
345
+ delete process.env.SYSTEM_ACCESSTOKEN;
346
+ delete process.env.SYSTEM_PULLREQUEST_PULLREQUESTID;
347
+ delete process.env.BITBUCKET_REPO_SLUG;
348
+ delete process.env.BITBUCKET_PR_ID;
344
349
  });
345
350
 
346
351
  afterEach(() => {
347
- vi.restoreAllMocks();
352
+ process.env = originalEnv;
348
353
  });
349
354
 
350
- describe('valid providers', () => {
351
- it('should output github script to stdout', () => {
352
- const output = (msg) => process.stdout.write(msg + '\n');
353
-
354
- // Read actual github.sh script
355
- const __filename = fileURLToPath(import.meta.url);
356
- const __dirname = dirname(__filename);
357
- const scriptPath = join(__dirname, '..', 'scripts', 'github.sh');
358
- const scriptContent = readFileSync(scriptPath, 'utf8');
359
-
360
- // Simulate get-script command output
361
- output(scriptContent);
362
-
363
- // Verify script was output to stdout
364
- expect(stdoutSpy).toHaveBeenCalledWith(scriptContent + '\n');
365
- expect(stdoutSpy).toHaveBeenCalledTimes(1);
366
- });
355
+ it('should detect GitHub when GITHUB_TOKEN and GITHUB_REPOSITORY are set', () => {
356
+ process.env.GITHUB_TOKEN = 'ghp_test123';
357
+ process.env.GITHUB_REPOSITORY = 'owner/repo';
367
358
 
368
- it('should output bitbucket script to stdout', () => {
369
- const output = (msg) => process.stdout.write(msg + '\n');
359
+ expect(detectCIProvider()).toBe('github');
360
+ });
370
361
 
371
- // Read actual bitbucket.sh script
372
- const __filename = fileURLToPath(import.meta.url);
373
- const __dirname = dirname(__filename);
374
- const scriptPath = join(__dirname, '..', 'scripts', 'bitbucket.sh');
375
- const scriptContent = readFileSync(scriptPath, 'utf8');
362
+ it('should detect Azure when SYSTEM_ACCESSTOKEN and SYSTEM_PULLREQUEST_PULLREQUESTID are set', () => {
363
+ process.env.SYSTEM_ACCESSTOKEN = 'azure_token';
364
+ process.env.SYSTEM_PULLREQUEST_PULLREQUESTID = '123';
376
365
 
377
- // Simulate get-script command output
378
- output(scriptContent);
366
+ expect(detectCIProvider()).toBe('azure');
367
+ });
379
368
 
380
- // Verify script was output to stdout
381
- expect(stdoutSpy).toHaveBeenCalledWith(scriptContent + '\n');
382
- expect(stdoutSpy).toHaveBeenCalledTimes(1);
383
- });
369
+ it('should detect Bitbucket when BITBUCKET_REPO_SLUG and BITBUCKET_PR_ID are set', () => {
370
+ process.env.BITBUCKET_REPO_SLUG = 'my-repo';
371
+ process.env.BITBUCKET_PR_ID = '456';
384
372
 
385
- it('should output azure script to stdout', () => {
386
- const output = (msg) => process.stdout.write(msg + '\n');
373
+ expect(detectCIProvider()).toBe('bitbucket');
374
+ });
387
375
 
388
- // Read actual azure.sh script
389
- const __filename = fileURLToPath(import.meta.url);
390
- const __dirname = dirname(__filename);
391
- const scriptPath = join(__dirname, '..', 'scripts', 'azure.sh');
392
- const scriptContent = readFileSync(scriptPath, 'utf8');
376
+ it('should return null when no CI provider env vars are set', () => {
377
+ expect(detectCIProvider()).toBe(null);
378
+ });
393
379
 
394
- // Simulate get-script command output
395
- output(scriptContent);
380
+ it('should return null when only partial GitHub env vars are set', () => {
381
+ process.env.GITHUB_TOKEN = 'ghp_test123';
382
+ // GITHUB_REPOSITORY not set
396
383
 
397
- // Verify script was output to stdout
398
- expect(stdoutSpy).toHaveBeenCalledWith(scriptContent + '\n');
399
- expect(stdoutSpy).toHaveBeenCalledTimes(1);
400
- });
384
+ expect(detectCIProvider()).toBe(null);
401
385
  });
402
386
 
403
- describe('script completeness', () => {
404
- it('should output complete bash scripts with proper structure', () => {
405
- const __filename = fileURLToPath(import.meta.url);
406
- const __dirname = dirname(__filename);
387
+ it('should return null when only partial Azure env vars are set', () => {
388
+ process.env.SYSTEM_ACCESSTOKEN = 'azure_token';
389
+ // SYSTEM_PULLREQUEST_PULLREQUESTID not set
407
390
 
408
- // Test all three scripts
409
- const providers = ['github', 'bitbucket', 'azure'];
410
-
411
- providers.forEach((provider) => {
412
- const scriptPath = join(__dirname, '..', 'scripts', `${provider}.sh`);
413
- const scriptContent = readFileSync(scriptPath, 'utf8');
391
+ expect(detectCIProvider()).toBe(null);
392
+ });
414
393
 
415
- // Verify script starts with shebang
416
- expect(scriptContent).toMatch(/^#!/);
417
- expect(scriptContent).toContain('#!/usr/bin/env bash');
394
+ it('should return null when only partial Bitbucket env vars are set', () => {
395
+ process.env.BITBUCKET_REPO_SLUG = 'my-repo';
396
+ // BITBUCKET_PR_ID not set
418
397
 
419
- // Verify script has substantial content (not truncated)
420
- const lineCount = scriptContent.split('\n').length;
421
- expect(lineCount).toBeGreaterThan(400);
398
+ expect(detectCIProvider()).toBe(null);
399
+ });
422
400
 
423
- // Verify script ends properly (has exit statement)
424
- expect(scriptContent).toContain('exit');
425
- });
426
- });
401
+ it('should prioritize GitHub over other providers when multiple are set', () => {
402
+ // Set all providers
403
+ process.env.GITHUB_TOKEN = 'ghp_test123';
404
+ process.env.GITHUB_REPOSITORY = 'owner/repo';
405
+ process.env.SYSTEM_ACCESSTOKEN = 'azure_token';
406
+ process.env.SYSTEM_PULLREQUEST_PULLREQUESTID = '123';
407
+ process.env.BITBUCKET_REPO_SLUG = 'my-repo';
408
+ process.env.BITBUCKET_PR_ID = '456';
409
+
410
+ // GitHub should be detected first due to check order
411
+ expect(detectCIProvider()).toBe('github');
427
412
  });
413
+ });
428
414
 
429
- describe('error handling', () => {
430
- it('should reject invalid provider', () => {
431
- const invalidProvider = 'gitlab';
432
- const validProviders = ['github', 'bitbucket', 'azure'];
415
+ describe('--comment flag behavior', () => {
416
+ it('should skip confirmation when --comment is set', () => {
417
+ const options = { comment: true };
433
418
 
434
- // Test validation logic from index.js line 513
435
- const isValid = validProviders.includes(invalidProvider.toLowerCase());
419
+ // Logic from index.js: if (!options.json && !options.comment) { confirmAction... }
420
+ const shouldShowConfirmation = !options.json && !options.comment;
436
421
 
437
- expect(isValid).toBe(false);
422
+ expect(shouldShowConfirmation).toBe(false);
423
+ });
438
424
 
439
- // In the actual command, this would trigger process.exit(1)
440
- // We verify the validation logic correctly identifies invalid providers
441
- expect(['github', 'bitbucket', 'azure']).not.toContain(invalidProvider);
442
- });
425
+ it('should skip confirmation when both --json and --comment are set', () => {
426
+ const options = { json: true, comment: true };
443
427
 
444
- it('should accept valid providers case-insensitively', () => {
445
- const validProviders = ['github', 'bitbucket', 'azure'];
428
+ const shouldShowConfirmation = !options.json && !options.comment;
446
429
 
447
- // Test that providers work case-insensitively
448
- expect(validProviders.includes('GitHub'.toLowerCase())).toBe(true);
449
- expect(validProviders.includes('BITBUCKET'.toLowerCase())).toBe(true);
450
- expect(validProviders.includes('Azure'.toLowerCase())).toBe(true);
451
- });
430
+ expect(shouldShowConfirmation).toBe(false);
452
431
  });
453
432
 
454
- describe('bundled script files', () => {
455
- it('should have all provider scripts bundled and readable', () => {
456
- const __filename = fileURLToPath(import.meta.url);
457
- const __dirname = dirname(__filename);
433
+ it('should show confirmation when neither --json nor --comment is set', () => {
434
+ const options = {};
458
435
 
459
- const providers = ['github', 'bitbucket', 'azure'];
436
+ const shouldShowConfirmation = !options.json && !options.comment;
460
437
 
461
- providers.forEach((provider) => {
462
- const scriptPath = join(__dirname, '..', 'scripts', `${provider}.sh`);
463
-
464
- // Should not throw - file exists and is readable
465
- expect(() => {
466
- const content = readFileSync(scriptPath, 'utf8');
467
- expect(content.length).toBeGreaterThan(0);
468
- }).not.toThrow();
469
- });
470
- });
438
+ expect(shouldShowConfirmation).toBe(true);
471
439
  });
472
440
  });