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 +5 -20
- package/package.json +1 -1
- package/src/config.js +1 -20
- package/src/git-logic.js +7 -63
- package/src/git-logic.test.js +5 -48
- package/src/index.js +109 -140
- package/src/index.test.js +72 -104
package/README.md
CHANGED
|
@@ -12,7 +12,7 @@ AI-powered code review CLI - Keep your kode korekt
|
|
|
12
12
|
|
|
13
13
|
* **AI-Powered Analysis**: Get instant, intelligent code reviews with severity levels, categories, and actionable suggestions
|
|
14
14
|
* **Local Git Integration**: Works with committed changes, staged changes, and unstaged modifications
|
|
15
|
-
* **Ticket
|
|
15
|
+
* **Ticket Context Enrichment**: Server-side ticket extraction from branch names and commit messages (Jira & Azure DevOps)
|
|
16
16
|
* **Beautiful Output**: Color-coded issues with severity indicators, file locations, and suggested fixes
|
|
17
17
|
* **Ultra-Fast**: Short command syntax (`kk`) for maximum developer efficiency
|
|
18
18
|
|
|
@@ -28,7 +28,7 @@ Configure the CLI with your API credentials:
|
|
|
28
28
|
|
|
29
29
|
```bash
|
|
30
30
|
kk config --key YOUR_API_KEY
|
|
31
|
-
kk config --endpoint https://api.korekt.ai/review
|
|
31
|
+
kk config --endpoint https://api.korekt.ai/api/review
|
|
32
32
|
```
|
|
33
33
|
|
|
34
34
|
Run your first review:
|
|
@@ -43,8 +43,7 @@ kk stg
|
|
|
43
43
|
# Review only unstaged changes
|
|
44
44
|
kk diff
|
|
45
45
|
|
|
46
|
-
|
|
47
|
-
kk all
|
|
46
|
+
|
|
48
47
|
```
|
|
49
48
|
|
|
50
49
|
## Usage
|
|
@@ -56,10 +55,7 @@ kk all
|
|
|
56
55
|
kk config --key YOUR_API_KEY
|
|
57
56
|
|
|
58
57
|
# Set API endpoint
|
|
59
|
-
kk config --endpoint https://api.korekt.ai/review
|
|
60
|
-
|
|
61
|
-
# Set default ticket system (jira or ado)
|
|
62
|
-
kk config --ticket-system jira
|
|
58
|
+
kk config --endpoint https://api.korekt.ai/api/review
|
|
63
59
|
|
|
64
60
|
# Show current configuration
|
|
65
61
|
kk config --show
|
|
@@ -74,9 +70,6 @@ kk review
|
|
|
74
70
|
# Review against specific branch
|
|
75
71
|
kk review main
|
|
76
72
|
|
|
77
|
-
# Review with ticket system override
|
|
78
|
-
kk review main --ticket-system ado
|
|
79
|
-
|
|
80
73
|
# Review with ignored files
|
|
81
74
|
kk review main --ignore "*.lock" "dist/*"
|
|
82
75
|
|
|
@@ -93,16 +86,9 @@ kk stg
|
|
|
93
86
|
# Review unstaged changes only
|
|
94
87
|
kk diff
|
|
95
88
|
|
|
96
|
-
# Review all uncommitted changes
|
|
97
|
-
kk all
|
|
98
|
-
|
|
99
|
-
# Include untracked files
|
|
100
|
-
kk all --untracked
|
|
101
|
-
|
|
102
89
|
# JSON output works with all review commands
|
|
103
90
|
kk stg --json
|
|
104
91
|
kk diff --json
|
|
105
|
-
kk all --json
|
|
106
92
|
```
|
|
107
93
|
|
|
108
94
|
### Alternative Command
|
|
@@ -119,8 +105,7 @@ You can also configure using environment variables:
|
|
|
119
105
|
|
|
120
106
|
```bash
|
|
121
107
|
export KOREKT_API_KEY="your-api-key"
|
|
122
|
-
export KOREKT_API_ENDPOINT="https://api.korekt.ai/review
|
|
123
|
-
export KOREKT_TICKET_SYSTEM="jira"
|
|
108
|
+
export KOREKT_API_ENDPOINT="https://api.korekt.ai/api/review"
|
|
124
109
|
```
|
|
125
110
|
|
|
126
111
|
Note: Config file takes precedence over environment variables.
|
package/package.json
CHANGED
package/src/config.js
CHANGED
|
@@ -35,7 +35,7 @@ export function getApiEndpoint() {
|
|
|
35
35
|
const configEndpoint = config.get('apiEndpoint');
|
|
36
36
|
if (configEndpoint) return configEndpoint;
|
|
37
37
|
|
|
38
|
-
return process.env.KOREKT_API_ENDPOINT || 'https://api.korekt.ai/api/review
|
|
38
|
+
return process.env.KOREKT_API_ENDPOINT || 'https://api.korekt.ai/api/review';
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
/**
|
|
@@ -45,24 +45,6 @@ export function setApiEndpoint(endpoint) {
|
|
|
45
45
|
config.set('apiEndpoint', endpoint);
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
/**
|
|
49
|
-
* Get the ticket system from config or environment
|
|
50
|
-
* Priority: 1) config store, 2) .env file
|
|
51
|
-
*/
|
|
52
|
-
export function getTicketSystem() {
|
|
53
|
-
const configTicketSystem = config.get('ticketSystem');
|
|
54
|
-
if (configTicketSystem) return configTicketSystem;
|
|
55
|
-
|
|
56
|
-
return process.env.KOREKT_TICKET_SYSTEM || null;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* Set the ticket system in config store
|
|
61
|
-
*/
|
|
62
|
-
export function setTicketSystem(system) {
|
|
63
|
-
config.set('ticketSystem', system);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
48
|
/**
|
|
67
49
|
* Get all configuration
|
|
68
50
|
*/
|
|
@@ -70,6 +52,5 @@ export function getConfig() {
|
|
|
70
52
|
return {
|
|
71
53
|
apiKey: getApiKey(),
|
|
72
54
|
apiEndpoint: getApiEndpoint(),
|
|
73
|
-
ticketSystem: getTicketSystem(),
|
|
74
55
|
};
|
|
75
56
|
}
|
package/src/git-logic.js
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
import { execa } from 'execa';
|
|
2
2
|
import chalk from 'chalk';
|
|
3
|
-
import fs from 'fs';
|
|
4
|
-
import path from 'path';
|
|
5
3
|
|
|
6
4
|
/**
|
|
7
5
|
* Truncate content to a maximum number of lines using "head and tail".
|
|
@@ -136,16 +134,11 @@ export function parseNameStatus(output) {
|
|
|
136
134
|
}
|
|
137
135
|
|
|
138
136
|
/**
|
|
139
|
-
* Analyze uncommitted changes (staged
|
|
140
|
-
* @param {string} mode - 'staged'
|
|
141
|
-
* @param {string|null} ticketSystem - The ticket system to use (jira or ado), or null to skip ticket extraction
|
|
137
|
+
* Analyze uncommitted changes (staged or unstaged)
|
|
138
|
+
* @param {string} mode - 'staged' or 'unstaged'
|
|
142
139
|
* @returns {Object|null} - The payload object ready for API submission, or null on error
|
|
143
140
|
*/
|
|
144
|
-
export async function runUncommittedReview(
|
|
145
|
-
mode = 'all',
|
|
146
|
-
_ticketSystem = null,
|
|
147
|
-
includeUntracked = false
|
|
148
|
-
) {
|
|
141
|
+
export async function runUncommittedReview(mode = 'unstaged') {
|
|
149
142
|
try {
|
|
150
143
|
// 1. Get Repo URL, current branch name, and repository root
|
|
151
144
|
const { stdout: repoUrl } = await execa('git', ['remote', 'get-url', 'origin']);
|
|
@@ -167,67 +160,23 @@ export async function runUncommittedReview(
|
|
|
167
160
|
if (mode === 'staged') {
|
|
168
161
|
nameStatusOutput = await git('diff', '--cached', '--name-status');
|
|
169
162
|
console.error(chalk.gray('Analyzing staged changes...'));
|
|
170
|
-
} else
|
|
163
|
+
} else {
|
|
171
164
|
nameStatusOutput = await git('diff', '--name-status');
|
|
172
165
|
console.error(chalk.gray('Analyzing unstaged changes...'));
|
|
173
|
-
} else {
|
|
174
|
-
// mode === 'all': combine staged and unstaged
|
|
175
|
-
const staged = await git('diff', '--cached', '--name-status');
|
|
176
|
-
const unstaged = await git('diff', '--name-status');
|
|
177
|
-
nameStatusOutput = [staged, unstaged].filter(Boolean).join('\n');
|
|
178
|
-
console.error(chalk.gray('Analyzing all uncommitted changes...'));
|
|
179
166
|
}
|
|
180
167
|
|
|
181
168
|
const fileList = parseNameStatus(nameStatusOutput);
|
|
182
169
|
const changedFiles = [];
|
|
183
170
|
|
|
184
|
-
|
|
185
|
-
if (includeUntracked) {
|
|
186
|
-
console.error(chalk.gray('Analyzing untracked files...'));
|
|
187
|
-
const untrackedFilesOutput = await git('ls-files', '--others', '--exclude-standard');
|
|
188
|
-
const untrackedFiles = untrackedFilesOutput.split('\n').filter(Boolean);
|
|
189
|
-
|
|
190
|
-
for (const file of untrackedFiles) {
|
|
191
|
-
const fullPath = path.join(repoRootPath, file);
|
|
192
|
-
const content = fs.readFileSync(fullPath, 'utf-8');
|
|
193
|
-
const diff = content
|
|
194
|
-
.split('\n')
|
|
195
|
-
.map((line) => `+${line}`)
|
|
196
|
-
.join('\n');
|
|
197
|
-
changedFiles.push({
|
|
198
|
-
path: file,
|
|
199
|
-
status: 'A', // Untracked files are always additions
|
|
200
|
-
diff: diff,
|
|
201
|
-
content: '', // No old content
|
|
202
|
-
});
|
|
203
|
-
// Add to fileList to prevent duplication if it's also in nameStatusOutput (edge case)
|
|
204
|
-
fileList.push({ status: 'A', path: file, oldPath: file });
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
// Deduplicate file list before processing diffs
|
|
209
|
-
const processedPaths = new Set(changedFiles.map((f) => f.path));
|
|
210
|
-
const uniqueFileList = fileList.filter((file) => !processedPaths.has(file.path));
|
|
211
|
-
|
|
212
|
-
for (const file of uniqueFileList) {
|
|
171
|
+
for (const file of fileList) {
|
|
213
172
|
const { status, path, oldPath } = file;
|
|
214
173
|
|
|
215
174
|
// Get diff for this file
|
|
216
175
|
let diff;
|
|
217
176
|
if (mode === 'staged') {
|
|
218
177
|
diff = await git('diff', '--cached', '-U15', '--', path);
|
|
219
|
-
} else if (mode === 'unstaged') {
|
|
220
|
-
diff = await git('diff', '-U15', '--', path);
|
|
221
178
|
} else {
|
|
222
|
-
|
|
223
|
-
try {
|
|
224
|
-
diff = await git('diff', '--cached', '-U15', '--', path);
|
|
225
|
-
if (!diff) {
|
|
226
|
-
diff = await git('diff', '-U15', '--', path);
|
|
227
|
-
}
|
|
228
|
-
} catch {
|
|
229
|
-
diff = await git('diff', '-U15', '--', path);
|
|
230
|
-
}
|
|
179
|
+
diff = await git('diff', '-U15', '--', path);
|
|
231
180
|
}
|
|
232
181
|
|
|
233
182
|
// Get current content from HEAD (before changes)
|
|
@@ -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']);
|
package/src/git-logic.test.js
CHANGED
|
@@ -83,7 +83,7 @@ describe('runUncommittedReview', () => {
|
|
|
83
83
|
throw new Error(`Unmocked command: ${command}`);
|
|
84
84
|
});
|
|
85
85
|
|
|
86
|
-
const result = await runUncommittedReview('staged'
|
|
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'
|
|
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('
|
|
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
|
|
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'
|
|
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 {
|
|
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((
|
|
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
|
-
|
|
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
|
-
--
|
|
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
|
|
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,
|
|
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
|
|
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.
|
|
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
|
|
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('
|
|
340
|
-
|
|
336
|
+
describe('detectCIProvider', () => {
|
|
337
|
+
const originalEnv = process.env;
|
|
341
338
|
|
|
342
339
|
beforeEach(() => {
|
|
343
|
-
|
|
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
|
-
|
|
352
|
+
process.env = originalEnv;
|
|
348
353
|
});
|
|
349
354
|
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
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
|
-
|
|
369
|
-
|
|
359
|
+
expect(detectCIProvider()).toBe('github');
|
|
360
|
+
});
|
|
370
361
|
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
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
|
-
|
|
378
|
-
|
|
366
|
+
expect(detectCIProvider()).toBe('azure');
|
|
367
|
+
});
|
|
379
368
|
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
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
|
-
|
|
386
|
-
|
|
373
|
+
expect(detectCIProvider()).toBe('bitbucket');
|
|
374
|
+
});
|
|
387
375
|
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
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
|
-
|
|
395
|
-
|
|
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
|
-
|
|
398
|
-
expect(stdoutSpy).toHaveBeenCalledWith(scriptContent + '\n');
|
|
399
|
-
expect(stdoutSpy).toHaveBeenCalledTimes(1);
|
|
400
|
-
});
|
|
384
|
+
expect(detectCIProvider()).toBe(null);
|
|
401
385
|
});
|
|
402
386
|
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
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
|
-
|
|
409
|
-
|
|
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
|
-
|
|
416
|
-
|
|
417
|
-
|
|
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
|
-
|
|
420
|
-
|
|
421
|
-
expect(lineCount).toBeGreaterThan(400);
|
|
398
|
+
expect(detectCIProvider()).toBe(null);
|
|
399
|
+
});
|
|
422
400
|
|
|
423
|
-
|
|
424
|
-
|
|
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
|
-
|
|
430
|
-
|
|
431
|
-
|
|
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
|
-
|
|
435
|
-
|
|
419
|
+
// Logic from index.js: if (!options.json && !options.comment) { confirmAction... }
|
|
420
|
+
const shouldShowConfirmation = !options.json && !options.comment;
|
|
436
421
|
|
|
437
|
-
|
|
422
|
+
expect(shouldShowConfirmation).toBe(false);
|
|
423
|
+
});
|
|
438
424
|
|
|
439
|
-
|
|
440
|
-
|
|
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
|
-
|
|
445
|
-
const validProviders = ['github', 'bitbucket', 'azure'];
|
|
428
|
+
const shouldShowConfirmation = !options.json && !options.comment;
|
|
446
429
|
|
|
447
|
-
|
|
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
|
-
|
|
455
|
-
|
|
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
|
-
|
|
436
|
+
const shouldShowConfirmation = !options.json && !options.comment;
|
|
460
437
|
|
|
461
|
-
|
|
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
|
});
|