cciwon-code-review-cli 2.0.6 → 2.0.7
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/.ipynb_checkpoints/package-checkpoint.json +36 -0
- package/bin/.ipynb_checkpoints/code-review-checkpoint.js +338 -0
- package/bin/code-review.js +1 -159
- package/lib/.ipynb_checkpoints/api-client-checkpoint.js +91 -0
- package/lib/.ipynb_checkpoints/chat-mode-checkpoint.js +245 -0
- package/lib/.ipynb_checkpoints/diff-viewer-checkpoint.js +14 -0
- package/lib/.ipynb_checkpoints/file-reader-checkpoint.js +159 -0
- package/lib/chat-mode.js +4 -0
- package/lib/diff-viewer.js +167 -0
- package/package.json +1 -1
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "cciwon-code-review-cli",
|
|
3
|
+
"version": "2.0.6",
|
|
4
|
+
"description": "AI-powered code review CLI tool using Qwen3-Coder-30B model with IP whitelist support",
|
|
5
|
+
"main": "lib/api-client.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"code-review": "bin/code-review.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
11
|
+
},
|
|
12
|
+
"keywords": [
|
|
13
|
+
"code-review",
|
|
14
|
+
"ai",
|
|
15
|
+
"qwen",
|
|
16
|
+
"qwen3-coder",
|
|
17
|
+
"code-analysis",
|
|
18
|
+
"security",
|
|
19
|
+
"llm",
|
|
20
|
+
"code-quality",
|
|
21
|
+
"static-analysis"
|
|
22
|
+
],
|
|
23
|
+
"author": "cciwon",
|
|
24
|
+
"license": "MIT",
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"axios": "^1.6.0",
|
|
27
|
+
"commander": "^11.1.0",
|
|
28
|
+
"chalk": "^4.1.2",
|
|
29
|
+
"ora": "^5.4.1",
|
|
30
|
+
"fast-glob": "^3.3.2",
|
|
31
|
+
"readline": "^1.3.0"
|
|
32
|
+
},
|
|
33
|
+
"engines": {
|
|
34
|
+
"node": ">=14.0.0"
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// bin/code-review.js
|
|
3
|
+
const { Command } = require('commander');
|
|
4
|
+
const chalk = require('chalk');
|
|
5
|
+
const ora = require('ora');
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
const { execSync } = require('child_process');
|
|
9
|
+
const os = require('os');
|
|
10
|
+
const CodeReviewClient = require('../lib/api-client');
|
|
11
|
+
const { parseDiffAndShowInVSCode } = require('../lib/diff-viewer');
|
|
12
|
+
|
|
13
|
+
const program = new Command();
|
|
14
|
+
|
|
15
|
+
// CLI 설정
|
|
16
|
+
program
|
|
17
|
+
.name('code-review')
|
|
18
|
+
.description('AI-powered code review CLI using Qwen3-Coder model')
|
|
19
|
+
.version('1.0.0');
|
|
20
|
+
|
|
21
|
+
// 서버 URL 설정 (환경변수 또는 기본값)
|
|
22
|
+
const SERVER_URL = process.env.CODE_REVIEW_SERVER || 'http://211.56.247.71:8000';
|
|
23
|
+
const client = new CodeReviewClient(SERVER_URL);
|
|
24
|
+
|
|
25
|
+
// ==========================================
|
|
26
|
+
// Diff 파싱 및 VSCode 표시 함수
|
|
27
|
+
// ==========================================
|
|
28
|
+
function parseDiffAndShowInVSCode(reviewText, originalFilePath) {
|
|
29
|
+
const lines = reviewText.split('\n');
|
|
30
|
+
const diffs = [];
|
|
31
|
+
let currentDiff = null;
|
|
32
|
+
let reviewContent = [];
|
|
33
|
+
let inDiffBlock = false;
|
|
34
|
+
|
|
35
|
+
// 디버깅: 전체 응답 확인
|
|
36
|
+
console.log(chalk.gray('\n[디버그] 응답 텍스트 일부:'));
|
|
37
|
+
console.log(chalk.gray(reviewText.substring(0, 500) + '...\n'));
|
|
38
|
+
|
|
39
|
+
for (let i = 0; i < lines.length; i++) {
|
|
40
|
+
const line = lines[i];
|
|
41
|
+
|
|
42
|
+
// unified diff 시작 감지 (--- 와 +++ 패턴)
|
|
43
|
+
// "--- 원본" 또는 "--- a/file" 형식도 지원
|
|
44
|
+
if ((line.startsWith('---') || line.includes('--- ')) &&
|
|
45
|
+
i + 1 < lines.length &&
|
|
46
|
+
(lines[i + 1].startsWith('+++') || lines[i + 1].includes('+++ '))) {
|
|
47
|
+
inDiffBlock = true;
|
|
48
|
+
currentDiff = {
|
|
49
|
+
filename: null,
|
|
50
|
+
diffLines: [line, lines[i + 1]]
|
|
51
|
+
};
|
|
52
|
+
console.log(chalk.yellow(`\n[디버그] diff 블록 발견: ${line}`));
|
|
53
|
+
i++; // +++ 라인 건너뛰기
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// diff 블록 내부
|
|
58
|
+
if (inDiffBlock) {
|
|
59
|
+
if (line.startsWith('@@') || line.startsWith('-') || line.startsWith('+') || line.startsWith(' ')) {
|
|
60
|
+
currentDiff.diffLines.push(line);
|
|
61
|
+
} else if (line.trim() === '' || line.startsWith('===')) {
|
|
62
|
+
// diff 블록 종료
|
|
63
|
+
if (currentDiff) {
|
|
64
|
+
diffs.push(currentDiff);
|
|
65
|
+
console.log(chalk.yellow(`[디버그] diff 블록 종료 (라인 수: ${currentDiff.diffLines.length})`));
|
|
66
|
+
currentDiff = null;
|
|
67
|
+
}
|
|
68
|
+
inDiffBlock = false;
|
|
69
|
+
reviewContent.push(line);
|
|
70
|
+
} else {
|
|
71
|
+
currentDiff.diffLines.push(line);
|
|
72
|
+
}
|
|
73
|
+
} else {
|
|
74
|
+
// 일반 리뷰 텍스트
|
|
75
|
+
reviewContent.push(line);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// 마지막 diff 저장
|
|
80
|
+
if (currentDiff) {
|
|
81
|
+
diffs.push(currentDiff);
|
|
82
|
+
console.log(chalk.yellow(`[디버그] 마지막 diff 블록 저장 (라인 수: ${currentDiff.diffLines.length})`));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
console.log(chalk.cyan(`\n[디버그] 총 ${diffs.length}개의 diff 블록 발견\n`));
|
|
86
|
+
|
|
87
|
+
// diff가 있으면 VSCode로 표시
|
|
88
|
+
if (diffs.length > 0) {
|
|
89
|
+
diffs.forEach((diff, index) => {
|
|
90
|
+
try {
|
|
91
|
+
// 임시 디렉토리 생성
|
|
92
|
+
const tmpDir = path.join(os.tmpdir(), `code-review-${Date.now()}-${index}`);
|
|
93
|
+
fs.mkdirSync(tmpDir, { recursive: true });
|
|
94
|
+
|
|
95
|
+
// 원본/수정본 파일 생성
|
|
96
|
+
const originalFile = path.join(tmpDir, 'original' + path.extname(originalFilePath));
|
|
97
|
+
const modifiedFile = path.join(tmpDir, 'modified' + path.extname(originalFilePath));
|
|
98
|
+
|
|
99
|
+
// diff에서 원본/수정본 코드 추출
|
|
100
|
+
const { original, modified } = extractCodeFromDiff(diff.diffLines, originalFilePath);
|
|
101
|
+
|
|
102
|
+
fs.writeFileSync(originalFile, original);
|
|
103
|
+
fs.writeFileSync(modifiedFile, modified);
|
|
104
|
+
|
|
105
|
+
// VSCode diff 뷰어 열기
|
|
106
|
+
try {
|
|
107
|
+
execSync(`code --diff "${originalFile}" "${modifiedFile}"`, { stdio: 'ignore' });
|
|
108
|
+
console.log(chalk.green(`\n📝 VSCode에서 diff를 열었습니다 (${index + 1}/${diffs.length})`));
|
|
109
|
+
} catch (e) {
|
|
110
|
+
console.log(chalk.yellow(`\n⚠️ VSCode를 열 수 없습니다. diff를 수동으로 확인하세요:`));
|
|
111
|
+
console.log(chalk.gray(diff.diffLines.join('\n')));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// 임시 파일은 5분 후 자동 삭제
|
|
115
|
+
setTimeout(() => {
|
|
116
|
+
try {
|
|
117
|
+
fs.unlinkSync(originalFile);
|
|
118
|
+
fs.unlinkSync(modifiedFile);
|
|
119
|
+
fs.rmdirSync(tmpDir);
|
|
120
|
+
} catch (e) {
|
|
121
|
+
// 무시
|
|
122
|
+
}
|
|
123
|
+
}, 5 * 60 * 1000);
|
|
124
|
+
|
|
125
|
+
} catch (error) {
|
|
126
|
+
console.error(chalk.red(`diff 처리 실패: ${error.message}`));
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// 리뷰 텍스트만 반환
|
|
132
|
+
return reviewContent.join('\n').trim();
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// diff에서 원본/수정본 추출
|
|
136
|
+
function extractCodeFromDiff(diffLines, originalFilePath) {
|
|
137
|
+
let original = '';
|
|
138
|
+
let modified = '';
|
|
139
|
+
|
|
140
|
+
// 원본 파일이 있으면 읽기
|
|
141
|
+
if (fs.existsSync(originalFilePath)) {
|
|
142
|
+
original = fs.readFileSync(originalFilePath, 'utf-8');
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// diff 라인을 파싱해서 수정본 생성
|
|
146
|
+
const originalLines = original.split('\n');
|
|
147
|
+
const modifiedLines = [];
|
|
148
|
+
|
|
149
|
+
let lineNum = 0;
|
|
150
|
+
for (const line of diffLines) {
|
|
151
|
+
if (line.startsWith('@@')) {
|
|
152
|
+
// @@ -15,2 +15,2 @@ 형식 파싱
|
|
153
|
+
const match = line.match(/@@ -(\d+),?(\d*) \+(\d+),?(\d*) @@/);
|
|
154
|
+
if (match) {
|
|
155
|
+
lineNum = parseInt(match[1]) - 1;
|
|
156
|
+
}
|
|
157
|
+
} else if (line.startsWith('-')) {
|
|
158
|
+
// 삭제된 라인 (원본에만 있음)
|
|
159
|
+
lineNum++;
|
|
160
|
+
} else if (line.startsWith('+')) {
|
|
161
|
+
// 추가된 라인 (수정본에만 있음)
|
|
162
|
+
modifiedLines.push(line.substring(1));
|
|
163
|
+
} else if (line.startsWith(' ')) {
|
|
164
|
+
// 변경 없는 라인
|
|
165
|
+
if (lineNum < originalLines.length) {
|
|
166
|
+
modifiedLines.push(originalLines[lineNum]);
|
|
167
|
+
lineNum++;
|
|
168
|
+
} else {
|
|
169
|
+
modifiedLines.push(line.substring(1));
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// 수정본이 비어있으면 원본을 기반으로 단순 패치 시도
|
|
175
|
+
if (modifiedLines.length === 0) {
|
|
176
|
+
modified = original;
|
|
177
|
+
} else {
|
|
178
|
+
modified = modifiedLines.join('\n');
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return { original, modified };
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// ==========================================
|
|
185
|
+
// 폴더 리뷰 명령어
|
|
186
|
+
// ==========================================
|
|
187
|
+
program
|
|
188
|
+
.command('folder <path>')
|
|
189
|
+
.description('폴더 전체를 리뷰합니다')
|
|
190
|
+
.option('-i, --include <patterns>', '포함할 파일 패턴 (쉼표로 구분)', '*.py,*.js,*.ts,*.java,*.cpp,*.go')
|
|
191
|
+
.option('-e, --exclude <patterns>', '제외할 패턴 (쉼표로 구분)', 'node_modules,__pycache__,.git,*.pyc')
|
|
192
|
+
.option('-m, --max-files <number>', '최대 파일 수', '50')
|
|
193
|
+
.action(async (folderPath, options) => {
|
|
194
|
+
const spinner = ora('폴더 리뷰 진행 중...').start();
|
|
195
|
+
|
|
196
|
+
try {
|
|
197
|
+
// 절대 경로로 변환
|
|
198
|
+
const absolutePath = path.resolve(folderPath);
|
|
199
|
+
|
|
200
|
+
// 폴더 존재 확인
|
|
201
|
+
if (!fs.existsSync(absolutePath)) {
|
|
202
|
+
spinner.fail(chalk.red(`❌ 폴더가 존재하지 않습니다: ${absolutePath}`));
|
|
203
|
+
process.exit(1);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// API 호출
|
|
207
|
+
const result = await client.reviewFolder(absolutePath, {
|
|
208
|
+
includePatterns: options.include.split(','),
|
|
209
|
+
excludePatterns: options.exclude.split(','),
|
|
210
|
+
maxFiles: parseInt(options.maxFiles)
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
spinner.succeed(chalk.green(`✅ 리뷰 완료! (분석된 파일: ${result.files_analyzed}개)`));
|
|
214
|
+
|
|
215
|
+
// 결과 출력
|
|
216
|
+
console.log('\n' + chalk.bold.cyan('=== 코드 리뷰 결과 ==='));
|
|
217
|
+
console.log(result.review);
|
|
218
|
+
|
|
219
|
+
if (result.warnings.length > 0) {
|
|
220
|
+
console.log('\n' + chalk.yellow('⚠️ 경고:'));
|
|
221
|
+
result.warnings.forEach(warning => {
|
|
222
|
+
console.log(chalk.yellow(` - ${warning}`));
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
} catch (error) {
|
|
227
|
+
spinner.fail(chalk.red('❌ 리뷰 실패'));
|
|
228
|
+
console.error(chalk.red(error.message));
|
|
229
|
+
process.exit(1);
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
// ==========================================
|
|
234
|
+
// 파일 리뷰 명령어
|
|
235
|
+
// ==========================================
|
|
236
|
+
program
|
|
237
|
+
.command('file <path>')
|
|
238
|
+
.description('단일 파일을 리뷰합니다')
|
|
239
|
+
.option('-l, --language <lang>', '프로그래밍 언어', 'python')
|
|
240
|
+
.option('-c, --checklist <path>', '체크리스트 파일 경로')
|
|
241
|
+
.action(async (filePath, options) => {
|
|
242
|
+
const spinner = ora('파일 리뷰 진행 중...').start();
|
|
243
|
+
|
|
244
|
+
try {
|
|
245
|
+
// 절대 경로로 변환
|
|
246
|
+
const absolutePath = path.resolve(filePath);
|
|
247
|
+
|
|
248
|
+
// 파일 존재 확인
|
|
249
|
+
if (!fs.existsSync(absolutePath)) {
|
|
250
|
+
spinner.fail(chalk.red(`❌ 파일이 존재하지 않습니다: ${absolutePath}`));
|
|
251
|
+
process.exit(1);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// 파일 읽기
|
|
255
|
+
const code = fs.readFileSync(absolutePath, 'utf-8');
|
|
256
|
+
|
|
257
|
+
// 체크리스트 읽기 (옵션)
|
|
258
|
+
let checklist = null;
|
|
259
|
+
if (options.checklist) {
|
|
260
|
+
const checklistPath = path.resolve(options.checklist);
|
|
261
|
+
if (fs.existsSync(checklistPath)) {
|
|
262
|
+
checklist = fs.readFileSync(checklistPath, 'utf-8');
|
|
263
|
+
} else {
|
|
264
|
+
spinner.warn(chalk.yellow(`⚠️ 체크리스트 파일을 찾을 수 없습니다: ${checklistPath}`));
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// API 호출
|
|
269
|
+
const result = await client.reviewCode(code, options.language, checklist);
|
|
270
|
+
|
|
271
|
+
spinner.succeed(chalk.green('✅ 리뷰 완료!'));
|
|
272
|
+
|
|
273
|
+
// diff를 파싱하고 VSCode로 표시, 리뷰 텍스트만 반환
|
|
274
|
+
const reviewTextOnly = parseDiffAndShowInVSCode(result.review, absolutePath);
|
|
275
|
+
|
|
276
|
+
// 결과 출력 (리뷰 텍스트만)
|
|
277
|
+
console.log('\n' + chalk.bold.cyan('=== 코드 리뷰 결과 ==='));
|
|
278
|
+
console.log(reviewTextOnly);
|
|
279
|
+
|
|
280
|
+
} catch (error) {
|
|
281
|
+
spinner.fail(chalk.red('❌ 리뷰 실패'));
|
|
282
|
+
console.error(chalk.red(error.message));
|
|
283
|
+
process.exit(1);
|
|
284
|
+
}
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
// ==========================================
|
|
288
|
+
// 헬스 체크 명령어
|
|
289
|
+
// ==========================================
|
|
290
|
+
program
|
|
291
|
+
.command('health')
|
|
292
|
+
.description('서버 상태를 확인합니다')
|
|
293
|
+
.action(async () => {
|
|
294
|
+
const spinner = ora('서버 연결 확인 중...').start();
|
|
295
|
+
|
|
296
|
+
try {
|
|
297
|
+
const health = await client.healthCheck();
|
|
298
|
+
spinner.succeed(chalk.green('✅ 서버 정상 작동 중'));
|
|
299
|
+
|
|
300
|
+
console.log(chalk.cyan('\n서버 정보:'));
|
|
301
|
+
console.log(` - 상태: ${chalk.green(health.status)}`);
|
|
302
|
+
console.log(` - 모델 로드: ${health.model_loaded ? chalk.green('완료') : chalk.red('실패')}`);
|
|
303
|
+
console.log(` - 서버 URL: ${chalk.blue(SERVER_URL)}`);
|
|
304
|
+
|
|
305
|
+
} catch (error) {
|
|
306
|
+
spinner.fail(chalk.red('❌ 서버 연결 실패'));
|
|
307
|
+
console.error(chalk.red(error.message));
|
|
308
|
+
process.exit(1);
|
|
309
|
+
}
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
// ==========================================
|
|
313
|
+
// 인터랙티브 Chat 모드
|
|
314
|
+
// ==========================================
|
|
315
|
+
program
|
|
316
|
+
.command('chat')
|
|
317
|
+
.description('대화형 코드 리뷰 모드를 시작합니다')
|
|
318
|
+
.action(async () => {
|
|
319
|
+
const ChatMode = require('../lib/chat-mode');
|
|
320
|
+
const chatMode = new ChatMode();
|
|
321
|
+
|
|
322
|
+
try {
|
|
323
|
+
await chatMode.start();
|
|
324
|
+
} catch (error) {
|
|
325
|
+
console.error(chalk.red(`\n❌ 에러 발생: ${error.message}`));
|
|
326
|
+
process.exit(1);
|
|
327
|
+
}
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
// ==========================================
|
|
331
|
+
// CLI 실행
|
|
332
|
+
// ==========================================
|
|
333
|
+
program.parse(process.argv);
|
|
334
|
+
|
|
335
|
+
// 명령어가 없으면 help 출력
|
|
336
|
+
if (!process.argv.slice(2).length) {
|
|
337
|
+
program.outputHelp();
|
|
338
|
+
}
|
package/bin/code-review.js
CHANGED
|
@@ -8,6 +8,7 @@ const path = require('path');
|
|
|
8
8
|
const { execSync } = require('child_process');
|
|
9
9
|
const os = require('os');
|
|
10
10
|
const CodeReviewClient = require('../lib/api-client');
|
|
11
|
+
const { parseDiffAndShowInVSCode } = require('../lib/diff-viewer');
|
|
11
12
|
|
|
12
13
|
const program = new Command();
|
|
13
14
|
|
|
@@ -21,165 +22,6 @@ program
|
|
|
21
22
|
const SERVER_URL = process.env.CODE_REVIEW_SERVER || 'http://211.56.247.71:8000';
|
|
22
23
|
const client = new CodeReviewClient(SERVER_URL);
|
|
23
24
|
|
|
24
|
-
// ==========================================
|
|
25
|
-
// Diff 파싱 및 VSCode 표시 함수
|
|
26
|
-
// ==========================================
|
|
27
|
-
function parseDiffAndShowInVSCode(reviewText, originalFilePath) {
|
|
28
|
-
const lines = reviewText.split('\n');
|
|
29
|
-
const diffs = [];
|
|
30
|
-
let currentDiff = null;
|
|
31
|
-
let reviewContent = [];
|
|
32
|
-
let inDiffBlock = false;
|
|
33
|
-
|
|
34
|
-
// 디버깅: 전체 응답 확인
|
|
35
|
-
console.log(chalk.gray('\n[디버그] 응답 텍스트 일부:'));
|
|
36
|
-
console.log(chalk.gray(reviewText.substring(0, 500) + '...\n'));
|
|
37
|
-
|
|
38
|
-
for (let i = 0; i < lines.length; i++) {
|
|
39
|
-
const line = lines[i];
|
|
40
|
-
|
|
41
|
-
// unified diff 시작 감지 (--- 와 +++ 패턴)
|
|
42
|
-
// "--- 원본" 또는 "--- a/file" 형식도 지원
|
|
43
|
-
if ((line.startsWith('---') || line.includes('--- ')) &&
|
|
44
|
-
i + 1 < lines.length &&
|
|
45
|
-
(lines[i + 1].startsWith('+++') || lines[i + 1].includes('+++ '))) {
|
|
46
|
-
inDiffBlock = true;
|
|
47
|
-
currentDiff = {
|
|
48
|
-
filename: null,
|
|
49
|
-
diffLines: [line, lines[i + 1]]
|
|
50
|
-
};
|
|
51
|
-
console.log(chalk.yellow(`\n[디버그] diff 블록 발견: ${line}`));
|
|
52
|
-
i++; // +++ 라인 건너뛰기
|
|
53
|
-
continue;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
// diff 블록 내부
|
|
57
|
-
if (inDiffBlock) {
|
|
58
|
-
if (line.startsWith('@@') || line.startsWith('-') || line.startsWith('+') || line.startsWith(' ')) {
|
|
59
|
-
currentDiff.diffLines.push(line);
|
|
60
|
-
} else if (line.trim() === '' || line.startsWith('===')) {
|
|
61
|
-
// diff 블록 종료
|
|
62
|
-
if (currentDiff) {
|
|
63
|
-
diffs.push(currentDiff);
|
|
64
|
-
console.log(chalk.yellow(`[디버그] diff 블록 종료 (라인 수: ${currentDiff.diffLines.length})`));
|
|
65
|
-
currentDiff = null;
|
|
66
|
-
}
|
|
67
|
-
inDiffBlock = false;
|
|
68
|
-
reviewContent.push(line);
|
|
69
|
-
} else {
|
|
70
|
-
currentDiff.diffLines.push(line);
|
|
71
|
-
}
|
|
72
|
-
} else {
|
|
73
|
-
// 일반 리뷰 텍스트
|
|
74
|
-
reviewContent.push(line);
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
// 마지막 diff 저장
|
|
79
|
-
if (currentDiff) {
|
|
80
|
-
diffs.push(currentDiff);
|
|
81
|
-
console.log(chalk.yellow(`[디버그] 마지막 diff 블록 저장 (라인 수: ${currentDiff.diffLines.length})`));
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
console.log(chalk.cyan(`\n[디버그] 총 ${diffs.length}개의 diff 블록 발견\n`));
|
|
85
|
-
|
|
86
|
-
// diff가 있으면 VSCode로 표시
|
|
87
|
-
if (diffs.length > 0) {
|
|
88
|
-
diffs.forEach((diff, index) => {
|
|
89
|
-
try {
|
|
90
|
-
// 임시 디렉토리 생성
|
|
91
|
-
const tmpDir = path.join(os.tmpdir(), `code-review-${Date.now()}-${index}`);
|
|
92
|
-
fs.mkdirSync(tmpDir, { recursive: true });
|
|
93
|
-
|
|
94
|
-
// 원본/수정본 파일 생성
|
|
95
|
-
const originalFile = path.join(tmpDir, 'original' + path.extname(originalFilePath));
|
|
96
|
-
const modifiedFile = path.join(tmpDir, 'modified' + path.extname(originalFilePath));
|
|
97
|
-
|
|
98
|
-
// diff에서 원본/수정본 코드 추출
|
|
99
|
-
const { original, modified } = extractCodeFromDiff(diff.diffLines, originalFilePath);
|
|
100
|
-
|
|
101
|
-
fs.writeFileSync(originalFile, original);
|
|
102
|
-
fs.writeFileSync(modifiedFile, modified);
|
|
103
|
-
|
|
104
|
-
// VSCode diff 뷰어 열기
|
|
105
|
-
try {
|
|
106
|
-
execSync(`code --diff "${originalFile}" "${modifiedFile}"`, { stdio: 'ignore' });
|
|
107
|
-
console.log(chalk.green(`\n📝 VSCode에서 diff를 열었습니다 (${index + 1}/${diffs.length})`));
|
|
108
|
-
} catch (e) {
|
|
109
|
-
console.log(chalk.yellow(`\n⚠️ VSCode를 열 수 없습니다. diff를 수동으로 확인하세요:`));
|
|
110
|
-
console.log(chalk.gray(diff.diffLines.join('\n')));
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
// 임시 파일은 5분 후 자동 삭제
|
|
114
|
-
setTimeout(() => {
|
|
115
|
-
try {
|
|
116
|
-
fs.unlinkSync(originalFile);
|
|
117
|
-
fs.unlinkSync(modifiedFile);
|
|
118
|
-
fs.rmdirSync(tmpDir);
|
|
119
|
-
} catch (e) {
|
|
120
|
-
// 무시
|
|
121
|
-
}
|
|
122
|
-
}, 5 * 60 * 1000);
|
|
123
|
-
|
|
124
|
-
} catch (error) {
|
|
125
|
-
console.error(chalk.red(`diff 처리 실패: ${error.message}`));
|
|
126
|
-
}
|
|
127
|
-
});
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
// 리뷰 텍스트만 반환
|
|
131
|
-
return reviewContent.join('\n').trim();
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
// diff에서 원본/수정본 추출
|
|
135
|
-
function extractCodeFromDiff(diffLines, originalFilePath) {
|
|
136
|
-
let original = '';
|
|
137
|
-
let modified = '';
|
|
138
|
-
|
|
139
|
-
// 원본 파일이 있으면 읽기
|
|
140
|
-
if (fs.existsSync(originalFilePath)) {
|
|
141
|
-
original = fs.readFileSync(originalFilePath, 'utf-8');
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
// diff 라인을 파싱해서 수정본 생성
|
|
145
|
-
const originalLines = original.split('\n');
|
|
146
|
-
const modifiedLines = [];
|
|
147
|
-
|
|
148
|
-
let lineNum = 0;
|
|
149
|
-
for (const line of diffLines) {
|
|
150
|
-
if (line.startsWith('@@')) {
|
|
151
|
-
// @@ -15,2 +15,2 @@ 형식 파싱
|
|
152
|
-
const match = line.match(/@@ -(\d+),?(\d*) \+(\d+),?(\d*) @@/);
|
|
153
|
-
if (match) {
|
|
154
|
-
lineNum = parseInt(match[1]) - 1;
|
|
155
|
-
}
|
|
156
|
-
} else if (line.startsWith('-')) {
|
|
157
|
-
// 삭제된 라인 (원본에만 있음)
|
|
158
|
-
lineNum++;
|
|
159
|
-
} else if (line.startsWith('+')) {
|
|
160
|
-
// 추가된 라인 (수정본에만 있음)
|
|
161
|
-
modifiedLines.push(line.substring(1));
|
|
162
|
-
} else if (line.startsWith(' ')) {
|
|
163
|
-
// 변경 없는 라인
|
|
164
|
-
if (lineNum < originalLines.length) {
|
|
165
|
-
modifiedLines.push(originalLines[lineNum]);
|
|
166
|
-
lineNum++;
|
|
167
|
-
} else {
|
|
168
|
-
modifiedLines.push(line.substring(1));
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
// 수정본이 비어있으면 원본을 기반으로 단순 패치 시도
|
|
174
|
-
if (modifiedLines.length === 0) {
|
|
175
|
-
modified = original;
|
|
176
|
-
} else {
|
|
177
|
-
modified = modifiedLines.join('\n');
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
return { original, modified };
|
|
181
|
-
}
|
|
182
|
-
|
|
183
25
|
// ==========================================
|
|
184
26
|
// 폴더 리뷰 명령어
|
|
185
27
|
// ==========================================
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
// lib/api-client.js
|
|
2
|
+
const axios = require('axios');
|
|
3
|
+
|
|
4
|
+
class CodeReviewClient {
|
|
5
|
+
constructor(serverUrl = 'http://211.56.247.71:8000') {
|
|
6
|
+
this.serverUrl = serverUrl;
|
|
7
|
+
this.client = axios.create({
|
|
8
|
+
baseURL: serverUrl,
|
|
9
|
+
timeout: 300000, // 5분 타임아웃 (모델 추론 시간 고려)
|
|
10
|
+
headers: {
|
|
11
|
+
'Content-Type': 'application/json'
|
|
12
|
+
}
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* 서버 헬스 체크
|
|
18
|
+
*/
|
|
19
|
+
async healthCheck() {
|
|
20
|
+
try {
|
|
21
|
+
const response = await this.client.get('/health');
|
|
22
|
+
return response.data;
|
|
23
|
+
} catch (error) {
|
|
24
|
+
this.handleError(error);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* 폴더 전체 리뷰
|
|
30
|
+
* @param {string} folderPath - 리뷰할 폴더 경로
|
|
31
|
+
* @param {object} options - 옵션 (include_patterns, exclude_patterns, max_files)
|
|
32
|
+
*/
|
|
33
|
+
async reviewFolder(folderPath, options = {}) {
|
|
34
|
+
try {
|
|
35
|
+
const response = await this.client.post('/review/folder', {
|
|
36
|
+
folder_path: folderPath,
|
|
37
|
+
include_patterns: options.includePatterns || ['*.py', '*.js', '*.ts', '*.java', '*.cpp', '*.go'],
|
|
38
|
+
exclude_patterns: options.excludePatterns || ['node_modules', '__pycache__', '.git', '*.pyc'],
|
|
39
|
+
max_files: options.maxFiles || 50
|
|
40
|
+
});
|
|
41
|
+
return response.data;
|
|
42
|
+
} catch (error) {
|
|
43
|
+
this.handleError(error);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* 단일 코드 스니펫 리뷰
|
|
49
|
+
* @param {string} code - 리뷰할 코드
|
|
50
|
+
* @param {string} language - 프로그래밍 언어
|
|
51
|
+
* @param {string} checklist - 체크리스트 (선택)
|
|
52
|
+
*/
|
|
53
|
+
async reviewCode(code, language = 'python', checklist = null) {
|
|
54
|
+
try {
|
|
55
|
+
const response = await this.client.post('/review/code', {
|
|
56
|
+
code,
|
|
57
|
+
language,
|
|
58
|
+
checklist
|
|
59
|
+
});
|
|
60
|
+
return response.data;
|
|
61
|
+
} catch (error) {
|
|
62
|
+
this.handleError(error);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* 에러 핸들링
|
|
68
|
+
*/
|
|
69
|
+
handleError(error) {
|
|
70
|
+
if (error.response) {
|
|
71
|
+
// 서버 응답이 있는 경우
|
|
72
|
+
const data = error.response.data;
|
|
73
|
+
|
|
74
|
+
// IP 차단 메시지
|
|
75
|
+
if (data.error === '허용된 아이피가 아닙니다') {
|
|
76
|
+
throw new Error(`❌ ${data.error}\n현재 IP: ${data.client_ip}\n허용된 IP: ${data.allowed_ips.join(', ')}`);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// 기타 서버 에러
|
|
80
|
+
throw new Error(`서버 에러 (${error.response.status}): ${data.detail || JSON.stringify(data)}`);
|
|
81
|
+
} else if (error.request) {
|
|
82
|
+
// 요청은 보냈지만 응답이 없는 경우
|
|
83
|
+
throw new Error(`서버 연결 실패: ${this.serverUrl}\n서버가 실행 중인지 확인하세요.`);
|
|
84
|
+
} else {
|
|
85
|
+
// 요청 설정 중 에러
|
|
86
|
+
throw new Error(`요청 실패: ${error.message}`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
module.exports = CodeReviewClient;
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
// lib/chat-mode.js
|
|
2
|
+
const readline = require('readline');
|
|
3
|
+
const chalk = require('chalk');
|
|
4
|
+
const FileReader = require('./file-reader');
|
|
5
|
+
const CodeReviewClient = require('./api-client');
|
|
6
|
+
const { parseDiffAndShowInVSCode } = require('./diff-viewer');
|
|
7
|
+
|
|
8
|
+
class ChatMode {
|
|
9
|
+
constructor() {
|
|
10
|
+
this.fileReader = new FileReader();
|
|
11
|
+
// 환경변수가 있으면 사용, 없으면 기본값 사용
|
|
12
|
+
const serverUrl = process.env.CODE_REVIEW_SERVER;
|
|
13
|
+
this.client = serverUrl ? new CodeReviewClient(serverUrl) : new CodeReviewClient();
|
|
14
|
+
this.projectFiles = [];
|
|
15
|
+
this.filesContent = {};
|
|
16
|
+
this.rl = null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Chat 모드 시작
|
|
21
|
+
*/
|
|
22
|
+
async start() {
|
|
23
|
+
console.log(chalk.bold.green('\n🛡️ 안녕하세요, 코드가드입니다!\n'));
|
|
24
|
+
|
|
25
|
+
// 프로젝트 파일 분석 여부 물어보기
|
|
26
|
+
const shouldAnalyze = await this.askQuestion('현재 위치의 폴더 구조를 파악할까요? (y/n): ');
|
|
27
|
+
|
|
28
|
+
if (shouldAnalyze.toLowerCase() === 'y' || shouldAnalyze.toLowerCase() === 'yes') {
|
|
29
|
+
await this.analyzeProject();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// REPL 시작
|
|
33
|
+
await this.startREPL();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* 프로젝트 파일 분석
|
|
38
|
+
*/
|
|
39
|
+
async analyzeProject() {
|
|
40
|
+
console.log(chalk.cyan('\n📂 폴더 구조 분석 중...\n'));
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
this.projectFiles = await this.fileReader.listFiles();
|
|
44
|
+
|
|
45
|
+
if (this.projectFiles.length === 0) {
|
|
46
|
+
console.log(chalk.yellow('⚠️ 분석할 파일이 없습니다.'));
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// 폴더 트리 출력
|
|
51
|
+
console.log(chalk.bold('프로젝트 구조:'));
|
|
52
|
+
const tree = this.fileReader.generateTree(this.projectFiles);
|
|
53
|
+
console.log(tree);
|
|
54
|
+
|
|
55
|
+
console.log(chalk.green(`✅ 총 ${this.projectFiles.length}개 파일 발견\n`));
|
|
56
|
+
} catch (error) {
|
|
57
|
+
console.error(chalk.red(`❌ 파일 분석 실패: ${error.message}`));
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* REPL 모드 시작
|
|
63
|
+
*/
|
|
64
|
+
async startREPL() {
|
|
65
|
+
this.rl = readline.createInterface({
|
|
66
|
+
input: process.stdin,
|
|
67
|
+
output: process.stdout,
|
|
68
|
+
prompt: chalk.bold.cyan('> ')
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
console.log(chalk.gray('무엇을 도와드릴까요? (종료하려면 "exit" 입력)\n'));
|
|
72
|
+
this.rl.prompt();
|
|
73
|
+
|
|
74
|
+
this.rl.on('line', async (input) => {
|
|
75
|
+
const command = input.trim();
|
|
76
|
+
|
|
77
|
+
if (!command) {
|
|
78
|
+
this.rl.prompt();
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (command === 'exit' || command === 'quit') {
|
|
83
|
+
console.log(chalk.green('\n👋 안녕히 가세요!'));
|
|
84
|
+
this.rl.close();
|
|
85
|
+
process.exit(0);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
await this.handleCommand(command);
|
|
89
|
+
this.rl.prompt();
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
this.rl.on('close', () => {
|
|
93
|
+
process.exit(0);
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* 사용자 명령어 처리
|
|
99
|
+
*/
|
|
100
|
+
async handleCommand(command) {
|
|
101
|
+
const lowerCommand = command.toLowerCase();
|
|
102
|
+
|
|
103
|
+
// 파일 리뷰 요청 감지
|
|
104
|
+
if (lowerCommand.includes('리뷰') || lowerCommand.includes('review') ||
|
|
105
|
+
lowerCommand.includes('검사') || lowerCommand.includes('check')) {
|
|
106
|
+
await this.handleReviewRequest(command);
|
|
107
|
+
}
|
|
108
|
+
// 파일 목록 요청
|
|
109
|
+
else if (lowerCommand.includes('목록') || lowerCommand.includes('list') ||
|
|
110
|
+
lowerCommand.includes('파일')) {
|
|
111
|
+
this.showFileList();
|
|
112
|
+
}
|
|
113
|
+
// 도움말
|
|
114
|
+
else if (lowerCommand.includes('help') || lowerCommand.includes('도움말')) {
|
|
115
|
+
this.showHelp();
|
|
116
|
+
}
|
|
117
|
+
// 기타 - 일반 질문으로 처리
|
|
118
|
+
else {
|
|
119
|
+
console.log(chalk.yellow('\n💡 파일을 리뷰하려면 "파일명 리뷰해줘" 형식으로 입력해주세요.'));
|
|
120
|
+
console.log(chalk.gray(' 예: api.py 리뷰해줘, 모든 Python 파일 검사해줘\n'));
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* 리뷰 요청 처리
|
|
126
|
+
*/
|
|
127
|
+
async handleReviewRequest(command) {
|
|
128
|
+
// 파일 매칭
|
|
129
|
+
const matchedFiles = this.fileReader.matchFiles(command, this.projectFiles);
|
|
130
|
+
|
|
131
|
+
if (matchedFiles.length === 0) {
|
|
132
|
+
console.log(chalk.yellow('\n⚠️ 매칭되는 파일을 찾을 수 없습니다.'));
|
|
133
|
+
console.log(chalk.gray(' 사용 가능한 파일 목록을 보려면 "파일 목록" 입력\n'));
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
console.log(chalk.cyan(`\n🔍 ${matchedFiles.length}개 파일 리뷰 중...\n`));
|
|
138
|
+
|
|
139
|
+
for (const filePath of matchedFiles) {
|
|
140
|
+
await this.reviewFile(filePath);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* 단일 파일 리뷰
|
|
146
|
+
*/
|
|
147
|
+
async reviewFile(filePath) {
|
|
148
|
+
try {
|
|
149
|
+
console.log(chalk.bold(`\n=== ${filePath} ===\n`));
|
|
150
|
+
|
|
151
|
+
// 파일 내용 읽기
|
|
152
|
+
console.log(chalk.gray(`📖 파일 읽는 중...`));
|
|
153
|
+
const content = await this.fileReader.readFile(filePath);
|
|
154
|
+
console.log(chalk.gray(`✓ 파일 읽기 완료 (${content.length}자)`));
|
|
155
|
+
|
|
156
|
+
// 언어 감지
|
|
157
|
+
const language = this.detectLanguage(filePath);
|
|
158
|
+
console.log(chalk.gray(`🔍 언어: ${language}`));
|
|
159
|
+
|
|
160
|
+
// 서버에 리뷰 요청
|
|
161
|
+
console.log(chalk.gray(`🚀 서버로 리뷰 요청 중...`));
|
|
162
|
+
const result = await this.client.reviewCode(content, language);
|
|
163
|
+
const reveiwTextOnly =
|
|
164
|
+
parserDiffAndShowInVSCode(result.reivew, filePath);
|
|
165
|
+
console.log(reviewTextOnly);
|
|
166
|
+
console.log(chalk.gray(`✓ 리뷰 완료\n`));
|
|
167
|
+
|
|
168
|
+
// 결과 출력
|
|
169
|
+
console.log(result.review);
|
|
170
|
+
console.log('');
|
|
171
|
+
|
|
172
|
+
} catch (error) {
|
|
173
|
+
console.error(chalk.red(`❌ 리뷰 실패 (${filePath}): ${error.message}\n`));
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* 파일 목록 출력
|
|
179
|
+
*/
|
|
180
|
+
showFileList() {
|
|
181
|
+
if (this.projectFiles.length === 0) {
|
|
182
|
+
console.log(chalk.yellow('\n⚠️ 분석된 파일이 없습니다. 먼저 프로젝트를 분석해주세요.\n'));
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
console.log(chalk.bold('\n📁 사용 가능한 파일:\n'));
|
|
187
|
+
this.projectFiles.forEach(file => {
|
|
188
|
+
console.log(chalk.gray(` - ${file}`));
|
|
189
|
+
});
|
|
190
|
+
console.log('');
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* 도움말 출력
|
|
195
|
+
*/
|
|
196
|
+
showHelp() {
|
|
197
|
+
console.log(chalk.bold('\n📖 사용 가능한 명령어:\n'));
|
|
198
|
+
console.log(chalk.cyan(' 파일명 리뷰해줘') + chalk.gray(' - 특정 파일 리뷰'));
|
|
199
|
+
console.log(chalk.cyan(' 모든 Python 파일 검사') + chalk.gray(' - 확장자별 리뷰'));
|
|
200
|
+
console.log(chalk.cyan(' 파일 목록') + chalk.gray(' - 분석 가능한 파일 목록'));
|
|
201
|
+
console.log(chalk.cyan(' help') + chalk.gray(' - 이 도움말'));
|
|
202
|
+
console.log(chalk.cyan(' exit') + chalk.gray(' - 종료\n'));
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* 파일 확장자로 언어 감지
|
|
207
|
+
*/
|
|
208
|
+
detectLanguage(filePath) {
|
|
209
|
+
const ext = filePath.split('.').pop().toLowerCase();
|
|
210
|
+
const langMap = {
|
|
211
|
+
'py': 'python',
|
|
212
|
+
'js': 'javascript',
|
|
213
|
+
'jsx': 'javascript',
|
|
214
|
+
'ts': 'typescript',
|
|
215
|
+
'tsx': 'typescript',
|
|
216
|
+
'java': 'java',
|
|
217
|
+
'cpp': 'cpp',
|
|
218
|
+
'c': 'c',
|
|
219
|
+
'go': 'go',
|
|
220
|
+
'rs': 'rust',
|
|
221
|
+
'rb': 'ruby',
|
|
222
|
+
'php': 'php'
|
|
223
|
+
};
|
|
224
|
+
return langMap[ext] || 'text';
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* 사용자에게 질문하고 답변 받기
|
|
229
|
+
*/
|
|
230
|
+
askQuestion(question) {
|
|
231
|
+
return new Promise((resolve) => {
|
|
232
|
+
const rl = readline.createInterface({
|
|
233
|
+
input: process.stdin,
|
|
234
|
+
output: process.stdout
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
rl.question(chalk.bold.cyan(question), (answer) => {
|
|
238
|
+
rl.close();
|
|
239
|
+
resolve(answer);
|
|
240
|
+
});
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
module.exports = ChatMode;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// lib/diff-viewer.js
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const os = require('os');
|
|
5
|
+
const { execSync } = require('child_process');
|
|
6
|
+
const chalk = require('chalk');
|
|
7
|
+
|
|
8
|
+
// ⚠️ 기존 bin/code-review.js에 있던
|
|
9
|
+
// parseDiffAndShowInVSCode + extractCodeFromDiff
|
|
10
|
+
// 그대로 복사해서 여기 붙여넣기
|
|
11
|
+
|
|
12
|
+
module.exports = {
|
|
13
|
+
parseDiffAndShowInVSCode,
|
|
14
|
+
};
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
// lib/file-reader.js
|
|
2
|
+
const fs = require('fs').promises;
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const fg = require('fast-glob');
|
|
5
|
+
|
|
6
|
+
class FileReader {
|
|
7
|
+
constructor(basePath = process.cwd()) {
|
|
8
|
+
this.basePath = basePath;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* 폴더 구조를 읽고 파일 목록 반환
|
|
13
|
+
* @param {object} options - { include, exclude, maxFiles }
|
|
14
|
+
* @returns {Promise<string[]>} 파일 경로 배열
|
|
15
|
+
*/
|
|
16
|
+
async listFiles(options = {}) {
|
|
17
|
+
const {
|
|
18
|
+
include = ['**/*.{js,ts,py,java,cpp,go,jsx,tsx}'],
|
|
19
|
+
exclude = ['**/node_modules/**', '**/__pycache__/**', '**/.git/**', '**/*.pyc'],
|
|
20
|
+
maxFiles = 50
|
|
21
|
+
} = options;
|
|
22
|
+
|
|
23
|
+
const files = await fg(include, {
|
|
24
|
+
cwd: this.basePath,
|
|
25
|
+
ignore: exclude,
|
|
26
|
+
absolute: false,
|
|
27
|
+
onlyFiles: true
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
return files.slice(0, maxFiles);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* 파일 내용 읽기
|
|
35
|
+
* @param {string} filePath - 상대 경로
|
|
36
|
+
* @returns {Promise<string>} 파일 내용
|
|
37
|
+
*/
|
|
38
|
+
async readFile(filePath) {
|
|
39
|
+
const fullPath = path.join(this.basePath, filePath);
|
|
40
|
+
try {
|
|
41
|
+
return await fs.readFile(fullPath, 'utf-8');
|
|
42
|
+
} catch (error) {
|
|
43
|
+
throw new Error(`파일 읽기 실패 (${filePath}): ${error.message}`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* 여러 파일의 내용을 읽어서 객체로 반환
|
|
49
|
+
* @param {string[]} filePaths - 파일 경로 배열
|
|
50
|
+
* @returns {Promise<object>} { 파일경로: 내용 }
|
|
51
|
+
*/
|
|
52
|
+
async readMultipleFiles(filePaths) {
|
|
53
|
+
const filesContent = {};
|
|
54
|
+
|
|
55
|
+
for (const filePath of filePaths) {
|
|
56
|
+
try {
|
|
57
|
+
filesContent[filePath] = await this.readFile(filePath);
|
|
58
|
+
} catch (error) {
|
|
59
|
+
console.warn(`⚠️ ${error.message}`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return filesContent;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* 폴더 구조 트리 생성 (시각화용)
|
|
68
|
+
* @param {string[]} filePaths - 파일 경로 배열
|
|
69
|
+
* @returns {string} 트리 형식 문자열
|
|
70
|
+
*/
|
|
71
|
+
generateTree(filePaths) {
|
|
72
|
+
const tree = {};
|
|
73
|
+
|
|
74
|
+
filePaths.forEach(filePath => {
|
|
75
|
+
const parts = filePath.split(path.sep);
|
|
76
|
+
let current = tree;
|
|
77
|
+
|
|
78
|
+
parts.forEach((part, index) => {
|
|
79
|
+
if (index === parts.length - 1) {
|
|
80
|
+
// 파일
|
|
81
|
+
if (!current.__files) current.__files = [];
|
|
82
|
+
current.__files.push(part);
|
|
83
|
+
} else {
|
|
84
|
+
// 디렉토리
|
|
85
|
+
if (!current[part]) current[part] = {};
|
|
86
|
+
current = current[part];
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
return this._renderTree(tree, '', true);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
_renderTree(node, prefix = '', isLast = true) {
|
|
95
|
+
let result = '';
|
|
96
|
+
const entries = Object.keys(node).filter(k => k !== '__files');
|
|
97
|
+
const files = node.__files || [];
|
|
98
|
+
|
|
99
|
+
// 디렉토리 먼저
|
|
100
|
+
entries.forEach((dir, index) => {
|
|
101
|
+
const isLastDir = index === entries.length - 1 && files.length === 0;
|
|
102
|
+
const connector = isLastDir ? '└── ' : '├── ';
|
|
103
|
+
const newPrefix = prefix + (isLastDir ? ' ' : '│ ');
|
|
104
|
+
|
|
105
|
+
result += `${prefix}${connector}${dir}/\n`;
|
|
106
|
+
result += this._renderTree(node[dir], newPrefix, isLastDir);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
// 파일들
|
|
110
|
+
files.forEach((file, index) => {
|
|
111
|
+
const isLastFile = index === files.length - 1;
|
|
112
|
+
const connector = isLastFile ? '└── ' : '├── ';
|
|
113
|
+
result += `${prefix}${connector}${file}\n`;
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
return result;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* 파일 경로에서 파일 찾기 (자연어 매칭)
|
|
121
|
+
* @param {string} query - "api.py", "모든 Python 파일"
|
|
122
|
+
* @param {string[]} availableFiles - 사용 가능한 파일 목록
|
|
123
|
+
* @returns {string[]} 매칭된 파일 경로들
|
|
124
|
+
*/
|
|
125
|
+
matchFiles(query, availableFiles) {
|
|
126
|
+
const lowerQuery = query.toLowerCase();
|
|
127
|
+
|
|
128
|
+
// 정확한 파일명 매칭
|
|
129
|
+
const exactMatch = availableFiles.filter(f =>
|
|
130
|
+
path.basename(f).toLowerCase() === lowerQuery ||
|
|
131
|
+
f.toLowerCase().includes(lowerQuery)
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
if (exactMatch.length > 0) return exactMatch;
|
|
135
|
+
|
|
136
|
+
// 확장자 매칭 ("모든 Python 파일", "py 파일들")
|
|
137
|
+
const extMap = {
|
|
138
|
+
'python': '.py',
|
|
139
|
+
'javascript': '.js',
|
|
140
|
+
'typescript': '.ts',
|
|
141
|
+
'java': '.java',
|
|
142
|
+
'cpp': '.cpp',
|
|
143
|
+
'go': '.go'
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
for (const [lang, ext] of Object.entries(extMap)) {
|
|
147
|
+
if (lowerQuery.includes(lang) || lowerQuery.includes(ext.slice(1))) {
|
|
148
|
+
return availableFiles.filter(f => f.endsWith(ext));
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// 키워드 매칭 ("api", "test", "model")
|
|
153
|
+
return availableFiles.filter(f =>
|
|
154
|
+
f.toLowerCase().includes(lowerQuery.replace(/\s+/g, ''))
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
module.exports = FileReader;
|
package/lib/chat-mode.js
CHANGED
|
@@ -3,6 +3,7 @@ const readline = require('readline');
|
|
|
3
3
|
const chalk = require('chalk');
|
|
4
4
|
const FileReader = require('./file-reader');
|
|
5
5
|
const CodeReviewClient = require('./api-client');
|
|
6
|
+
const { parseDiffAndShowInVSCode } = require('./diff-viewer');
|
|
6
7
|
|
|
7
8
|
class ChatMode {
|
|
8
9
|
constructor() {
|
|
@@ -159,6 +160,9 @@ class ChatMode {
|
|
|
159
160
|
// 서버에 리뷰 요청
|
|
160
161
|
console.log(chalk.gray(`🚀 서버로 리뷰 요청 중...`));
|
|
161
162
|
const result = await this.client.reviewCode(content, language);
|
|
163
|
+
const reveiwTextOnly =
|
|
164
|
+
parserDiffAndShowInVSCode(result.reivew, filePath);
|
|
165
|
+
console.log(reviewTextOnly);
|
|
162
166
|
console.log(chalk.gray(`✓ 리뷰 완료\n`));
|
|
163
167
|
|
|
164
168
|
// 결과 출력
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
// lib/diff-viewer.js
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const os = require('os');
|
|
5
|
+
const { execSync } = require('child_process');
|
|
6
|
+
const chalk = require('chalk');
|
|
7
|
+
|
|
8
|
+
// Diff 파싱 및 VSCode 표시 함수
|
|
9
|
+
function parseDiffAndShowInVSCode(reviewText, originalFilePath) {
|
|
10
|
+
const lines = reviewText.split('\n');
|
|
11
|
+
const diffs = [];
|
|
12
|
+
let currentDiff = null;
|
|
13
|
+
let reviewContent = [];
|
|
14
|
+
let inDiffBlock = false;
|
|
15
|
+
|
|
16
|
+
// 디버깅: 전체 응답 확인
|
|
17
|
+
console.log(chalk.gray('\n[디버그] 응답 텍스트 일부:'));
|
|
18
|
+
console.log(chalk.gray(reviewText.substring(0, 500) + '...\n'));
|
|
19
|
+
|
|
20
|
+
for (let i = 0; i < lines.length; i++) {
|
|
21
|
+
const line = lines[i];
|
|
22
|
+
|
|
23
|
+
// unified diff 시작 감지 (--- 와 +++ 패턴)
|
|
24
|
+
// "--- 원본" 또는 "--- a/file" 형식도 지원
|
|
25
|
+
if ((line.startsWith('---') || line.includes('--- ')) &&
|
|
26
|
+
i + 1 < lines.length &&
|
|
27
|
+
(lines[i + 1].startsWith('+++') || lines[i + 1].includes('+++ '))) {
|
|
28
|
+
inDiffBlock = true;
|
|
29
|
+
currentDiff = {
|
|
30
|
+
filename: null,
|
|
31
|
+
diffLines: [line, lines[i + 1]]
|
|
32
|
+
};
|
|
33
|
+
console.log(chalk.yellow(`\n[디버그] diff 블록 발견: ${line}`));
|
|
34
|
+
i++; // +++ 라인 건너뛰기
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// diff 블록 내부
|
|
39
|
+
if (inDiffBlock) {
|
|
40
|
+
if (line.startsWith('@@') || line.startsWith('-') || line.startsWith('+') || line.startsWith(' ')) {
|
|
41
|
+
currentDiff.diffLines.push(line);
|
|
42
|
+
} else if (line.trim() === '' || line.startsWith('===')) {
|
|
43
|
+
// diff 블록 종료
|
|
44
|
+
if (currentDiff) {
|
|
45
|
+
diffs.push(currentDiff);
|
|
46
|
+
console.log(chalk.yellow(`[디버그] diff 블록 종료 (라인 수: ${currentDiff.diffLines.length})`));
|
|
47
|
+
currentDiff = null;
|
|
48
|
+
}
|
|
49
|
+
inDiffBlock = false;
|
|
50
|
+
reviewContent.push(line);
|
|
51
|
+
} else {
|
|
52
|
+
currentDiff.diffLines.push(line);
|
|
53
|
+
}
|
|
54
|
+
} else {
|
|
55
|
+
// 일반 리뷰 텍스트
|
|
56
|
+
reviewContent.push(line);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// 마지막 diff 저장
|
|
61
|
+
if (currentDiff) {
|
|
62
|
+
diffs.push(currentDiff);
|
|
63
|
+
console.log(chalk.yellow(`[디버그] 마지막 diff 블록 저장 (라인 수: ${currentDiff.diffLines.length})`));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
console.log(chalk.cyan(`\n[디버그] 총 ${diffs.length}개의 diff 블록 발견\n`));
|
|
67
|
+
|
|
68
|
+
// diff가 있으면 VSCode로 표시
|
|
69
|
+
if (diffs.length > 0) {
|
|
70
|
+
diffs.forEach((diff, index) => {
|
|
71
|
+
try {
|
|
72
|
+
// 임시 디렉토리 생성
|
|
73
|
+
const tmpDir = path.join(os.tmpdir(), `code-review-${Date.now()}-${index}`);
|
|
74
|
+
fs.mkdirSync(tmpDir, { recursive: true });
|
|
75
|
+
|
|
76
|
+
// 원본/수정본 파일 생성
|
|
77
|
+
const originalFile = path.join(tmpDir, 'original' + path.extname(originalFilePath));
|
|
78
|
+
const modifiedFile = path.join(tmpDir, 'modified' + path.extname(originalFilePath));
|
|
79
|
+
|
|
80
|
+
// diff에서 원본/수정본 코드 추출
|
|
81
|
+
const { original, modified } = extractCodeFromDiff(diff.diffLines, originalFilePath);
|
|
82
|
+
|
|
83
|
+
fs.writeFileSync(originalFile, original);
|
|
84
|
+
fs.writeFileSync(modifiedFile, modified);
|
|
85
|
+
|
|
86
|
+
// VSCode diff 뷰어 열기
|
|
87
|
+
try {
|
|
88
|
+
execSync(`code --diff "${originalFile}" "${modifiedFile}"`, { stdio: 'ignore' });
|
|
89
|
+
console.log(chalk.green(`\n📝 VSCode에서 diff를 열었습니다 (${index + 1}/${diffs.length})`));
|
|
90
|
+
} catch (e) {
|
|
91
|
+
console.log(chalk.yellow(`\n⚠️ VSCode를 열 수 없습니다. diff를 수동으로 확인하세요:`));
|
|
92
|
+
console.log(chalk.gray(diff.diffLines.join('\n')));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// 임시 파일은 5분 후 자동 삭제
|
|
96
|
+
setTimeout(() => {
|
|
97
|
+
try {
|
|
98
|
+
fs.unlinkSync(originalFile);
|
|
99
|
+
fs.unlinkSync(modifiedFile);
|
|
100
|
+
fs.rmdirSync(tmpDir);
|
|
101
|
+
} catch (e) {
|
|
102
|
+
// 무시
|
|
103
|
+
}
|
|
104
|
+
}, 5 * 60 * 1000);
|
|
105
|
+
|
|
106
|
+
} catch (error) {
|
|
107
|
+
console.error(chalk.red(`diff 처리 실패: ${error.message}`));
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// 리뷰 텍스트만 반환
|
|
113
|
+
return reviewContent.join('\n').trim();
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// diff에서 원본/수정본 추출
|
|
117
|
+
function extractCodeFromDiff(diffLines, originalFilePath) {
|
|
118
|
+
let original = '';
|
|
119
|
+
let modified = '';
|
|
120
|
+
|
|
121
|
+
// 원본 파일이 있으면 읽기
|
|
122
|
+
if (fs.existsSync(originalFilePath)) {
|
|
123
|
+
original = fs.readFileSync(originalFilePath, 'utf-8');
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// diff 라인을 파싱해서 수정본 생성
|
|
127
|
+
const originalLines = original.split('\n');
|
|
128
|
+
const modifiedLines = [];
|
|
129
|
+
|
|
130
|
+
let lineNum = 0;
|
|
131
|
+
for (const line of diffLines) {
|
|
132
|
+
if (line.startsWith('@@')) {
|
|
133
|
+
// @@ -15,2 +15,2 @@ 형식 파싱
|
|
134
|
+
const match = line.match(/@@ -(\d+),?(\d*) \+(\d+),?(\d*) @@/);
|
|
135
|
+
if (match) {
|
|
136
|
+
lineNum = parseInt(match[1]) - 1;
|
|
137
|
+
}
|
|
138
|
+
} else if (line.startsWith('-')) {
|
|
139
|
+
// 삭제된 라인 (원본에만 있음)
|
|
140
|
+
lineNum++;
|
|
141
|
+
} else if (line.startsWith('+')) {
|
|
142
|
+
// 추가된 라인 (수정본에만 있음)
|
|
143
|
+
modifiedLines.push(line.substring(1));
|
|
144
|
+
} else if (line.startsWith(' ')) {
|
|
145
|
+
// 변경 없는 라인
|
|
146
|
+
if (lineNum < originalLines.length) {
|
|
147
|
+
modifiedLines.push(originalLines[lineNum]);
|
|
148
|
+
lineNum++;
|
|
149
|
+
} else {
|
|
150
|
+
modifiedLines.push(line.substring(1));
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// 수정본이 비어있으면 원본을 기반으로 단순 패치 시도
|
|
156
|
+
if (modifiedLines.length === 0) {
|
|
157
|
+
modified = original;
|
|
158
|
+
} else {
|
|
159
|
+
modified = modifiedLines.join('\n');
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return { original, modified };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
module.exports = {
|
|
166
|
+
parseDiffAndShowInVSCode,
|
|
167
|
+
};
|