cciwon-code-review-cli 1.0.0 → 2.0.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/.ipynb_checkpoints/README-checkpoint.md +233 -0
- package/bin/code-review.js +18 -0
- package/lib/api-client.js +1 -1
- package/lib/chat-mode.js +234 -0
- package/lib/file-reader.js +159 -0
- package/package.json +4 -2
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
# cciwon-code-review-cli
|
|
2
|
+
|
|
3
|
+
AI 기반 코드 리뷰 CLI 도구 (Qwen3-Coder-30B DPO 파인튜닝 모델 사용)
|
|
4
|
+
|
|
5
|
+
## 특징
|
|
6
|
+
|
|
7
|
+
- 🔒 **폐쇄망 지원**: IP 화이트리스트 기반 (192.168.10.152)
|
|
8
|
+
- 📁 **폴더 전체 리뷰**: 프로젝트 전체를 한 번에 분석
|
|
9
|
+
- 📄 **파일 단위 리뷰**: 개별 파일 심층 분석
|
|
10
|
+
- 🛡️ **보안 중심**: SQL Injection, XSS 등 보안 취약점 탐지
|
|
11
|
+
- ⚡ **빠른 응답**: vLLM 기반 고성능 추론
|
|
12
|
+
|
|
13
|
+
## 설치
|
|
14
|
+
|
|
15
|
+
### 1. 서버 설정
|
|
16
|
+
|
|
17
|
+
먼저 vLLM 서버를 시작해야 합니다:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
# 필수 패키지 설치
|
|
21
|
+
pip install vllm fastapi uvicorn
|
|
22
|
+
|
|
23
|
+
# 서버 실행
|
|
24
|
+
cd /home/cwk317/codereivew
|
|
25
|
+
python vllm_server.py
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
서버가 `192.168.10.152:8000`에서 실행됩니다.
|
|
29
|
+
|
|
30
|
+
### 2. CLI 설치
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
cd code-review-cli
|
|
34
|
+
npm install
|
|
35
|
+
npm link # 전역 설치
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
또는 로컬에서 직접 사용:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
node bin/code-review.js <command>
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## 사용법
|
|
45
|
+
|
|
46
|
+
### 서버 상태 확인
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
code-review health
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
출력 예시:
|
|
53
|
+
```
|
|
54
|
+
✅ 서버 정상 작동 중
|
|
55
|
+
|
|
56
|
+
서버 정보:
|
|
57
|
+
- 상태: healthy
|
|
58
|
+
- 모델 로드: 완료
|
|
59
|
+
- 서버 URL: http://192.168.10.152:8000
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### 폴더 전체 리뷰
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
code-review folder /path/to/project
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
옵션:
|
|
69
|
+
- `-i, --include <patterns>`: 포함할 파일 패턴 (기본: `*.py,*.js,*.ts,*.java,*.cpp,*.go`)
|
|
70
|
+
- `-e, --exclude <patterns>`: 제외할 패턴 (기본: `node_modules,__pycache__,.git,*.pyc`)
|
|
71
|
+
- `-m, --max-files <number>`: 최대 파일 수 (기본: 50)
|
|
72
|
+
|
|
73
|
+
예시:
|
|
74
|
+
```bash
|
|
75
|
+
# Python 프로젝트 리뷰
|
|
76
|
+
code-review folder ./my-python-project -i "*.py" -e "__pycache__,*.pyc,venv"
|
|
77
|
+
|
|
78
|
+
# JavaScript 프로젝트 리뷰
|
|
79
|
+
code-review folder ./my-js-project -i "*.js,*.jsx" -e "node_modules,dist,build"
|
|
80
|
+
|
|
81
|
+
# 모든 코드 파일 리뷰 (최대 100개)
|
|
82
|
+
code-review folder ./my-project -m 100
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### 단일 파일 리뷰
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
code-review file /path/to/file.py
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
옵션:
|
|
92
|
+
- `-l, --language <lang>`: 프로그래밍 언어 (기본: python)
|
|
93
|
+
- `-c, --checklist <path>`: 체크리스트 파일 경로
|
|
94
|
+
|
|
95
|
+
예시:
|
|
96
|
+
```bash
|
|
97
|
+
# Python 파일 리뷰
|
|
98
|
+
code-review file ./app.py -l python
|
|
99
|
+
|
|
100
|
+
# TypeScript 파일 + 체크리스트
|
|
101
|
+
code-review file ./api.ts -l typescript -c ./checklist.txt
|
|
102
|
+
|
|
103
|
+
# Java 파일 리뷰
|
|
104
|
+
code-review file ./Main.java -l java
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## VSCode 통합
|
|
108
|
+
|
|
109
|
+
### 방법 1: Tasks 사용
|
|
110
|
+
|
|
111
|
+
`.vscode/tasks.json` 파일 생성:
|
|
112
|
+
|
|
113
|
+
```json
|
|
114
|
+
{
|
|
115
|
+
"version": "2.0.0",
|
|
116
|
+
"tasks": [
|
|
117
|
+
{
|
|
118
|
+
"label": "Code Review: Current Folder",
|
|
119
|
+
"type": "shell",
|
|
120
|
+
"command": "code-review folder ${workspaceFolder}",
|
|
121
|
+
"problemMatcher": [],
|
|
122
|
+
"presentation": {
|
|
123
|
+
"reveal": "always",
|
|
124
|
+
"panel": "new"
|
|
125
|
+
}
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
"label": "Code Review: Current File",
|
|
129
|
+
"type": "shell",
|
|
130
|
+
"command": "code-review file ${file}",
|
|
131
|
+
"problemMatcher": [],
|
|
132
|
+
"presentation": {
|
|
133
|
+
"reveal": "always",
|
|
134
|
+
"panel": "new"
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
]
|
|
138
|
+
}
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
사용법:
|
|
142
|
+
1. `Ctrl+Shift+P` → `Tasks: Run Task`
|
|
143
|
+
2. `Code Review: Current Folder` 또는 `Code Review: Current File` 선택
|
|
144
|
+
|
|
145
|
+
### 방법 2: 터미널에서 직접 사용
|
|
146
|
+
|
|
147
|
+
VSCode 내장 터미널에서:
|
|
148
|
+
|
|
149
|
+
```bash
|
|
150
|
+
# 현재 워크스페이스 리뷰
|
|
151
|
+
code-review folder .
|
|
152
|
+
|
|
153
|
+
# 특정 파일 리뷰
|
|
154
|
+
code-review file src/main.py
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
## 환경 변수
|
|
158
|
+
|
|
159
|
+
서버 URL을 변경하려면 환경 변수 설정:
|
|
160
|
+
|
|
161
|
+
```bash
|
|
162
|
+
export CODE_REVIEW_SERVER=http://192.168.10.152:8000
|
|
163
|
+
code-review health
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
## IP 화이트리스트
|
|
167
|
+
|
|
168
|
+
현재 허용된 IP:
|
|
169
|
+
- `192.168.10.152` (서버 IP)
|
|
170
|
+
- `127.0.0.1` (로컬 테스트용)
|
|
171
|
+
|
|
172
|
+
다른 IP에서 접근 시:
|
|
173
|
+
```
|
|
174
|
+
❌ 허용된 아이피가 아닙니다
|
|
175
|
+
현재 IP: 192.168.10.xxx
|
|
176
|
+
허용된 IP: 192.168.10.152, 127.0.0.1
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
IP 추가하려면 `vllm_server.py`의 `ALLOWED_IPS` 수정:
|
|
180
|
+
|
|
181
|
+
```python
|
|
182
|
+
ALLOWED_IPS = ["192.168.10.152", "127.0.0.1", "192.168.10.100"] # 새 IP 추가
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
## 트러블슈팅
|
|
186
|
+
|
|
187
|
+
### 서버 연결 실패
|
|
188
|
+
|
|
189
|
+
```
|
|
190
|
+
❌ 서버 연결 실패
|
|
191
|
+
서버 연결 실패: http://192.168.10.152:8000
|
|
192
|
+
서버가 실행 중인지 확인하세요.
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
해결:
|
|
196
|
+
1. vLLM 서버가 실행 중인지 확인: `ps aux | grep vllm_server`
|
|
197
|
+
2. 서버 로그 확인: `tail -f /path/to/server.log`
|
|
198
|
+
3. 방화벽 확인: `sudo ufw status`
|
|
199
|
+
|
|
200
|
+
### IP 차단
|
|
201
|
+
|
|
202
|
+
```
|
|
203
|
+
❌ 허용된 아이피가 아닙니다
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
해결:
|
|
207
|
+
1. 현재 IP 확인: `curl ifconfig.me`
|
|
208
|
+
2. `vllm_server.py`의 `ALLOWED_IPS`에 추가
|
|
209
|
+
3. 서버 재시작
|
|
210
|
+
|
|
211
|
+
### 메모리 부족
|
|
212
|
+
|
|
213
|
+
vLLM 서버가 OOM으로 죽는 경우:
|
|
214
|
+
|
|
215
|
+
`vllm_server.py` 수정:
|
|
216
|
+
```python
|
|
217
|
+
llm = LLM(
|
|
218
|
+
model=OUTPUT_DIR,
|
|
219
|
+
tensor_parallel_size=2,
|
|
220
|
+
gpu_memory_utilization=0.7, # 0.9 → 0.7로 감소
|
|
221
|
+
max_model_len=1024, # 2048 → 1024로 감소
|
|
222
|
+
)
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
## 성능
|
|
226
|
+
|
|
227
|
+
- 평균 응답 시간: ~5초 (폴더 리뷰, 10개 파일 기준)
|
|
228
|
+
- 동시 처리: vLLM 배치 처리로 여러 요청 동시 처리 가능
|
|
229
|
+
- GPU 사용량: 2x RTX 5090 (50-60GB)
|
|
230
|
+
|
|
231
|
+
## 라이선스
|
|
232
|
+
|
|
233
|
+
MIT
|
package/bin/code-review.js
CHANGED
|
@@ -144,6 +144,24 @@ program
|
|
|
144
144
|
}
|
|
145
145
|
});
|
|
146
146
|
|
|
147
|
+
// ==========================================
|
|
148
|
+
// 인터랙티브 Chat 모드
|
|
149
|
+
// ==========================================
|
|
150
|
+
program
|
|
151
|
+
.command('chat')
|
|
152
|
+
.description('대화형 코드 리뷰 모드를 시작합니다')
|
|
153
|
+
.action(async () => {
|
|
154
|
+
const ChatMode = require('../lib/chat-mode');
|
|
155
|
+
const chatMode = new ChatMode();
|
|
156
|
+
|
|
157
|
+
try {
|
|
158
|
+
await chatMode.start();
|
|
159
|
+
} catch (error) {
|
|
160
|
+
console.error(chalk.red(`\n❌ 에러 발생: ${error.message}`));
|
|
161
|
+
process.exit(1);
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
|
|
147
165
|
// ==========================================
|
|
148
166
|
// CLI 실행
|
|
149
167
|
// ==========================================
|
package/lib/api-client.js
CHANGED
package/lib/chat-mode.js
ADDED
|
@@ -0,0 +1,234 @@
|
|
|
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
|
+
|
|
7
|
+
class ChatMode {
|
|
8
|
+
constructor() {
|
|
9
|
+
this.fileReader = new FileReader();
|
|
10
|
+
this.client = new CodeReviewClient(process.env.CODE_REVIEW_SERVER);
|
|
11
|
+
this.projectFiles = [];
|
|
12
|
+
this.filesContent = {};
|
|
13
|
+
this.rl = null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Chat 모드 시작
|
|
18
|
+
*/
|
|
19
|
+
async start() {
|
|
20
|
+
console.log(chalk.bold.green('\n🛡️ 안녕하세요, 코드가드입니다!\n'));
|
|
21
|
+
|
|
22
|
+
// 프로젝트 파일 분석 여부 물어보기
|
|
23
|
+
const shouldAnalyze = await this.askQuestion('현재 위치의 폴더 구조를 파악할까요? (y/n): ');
|
|
24
|
+
|
|
25
|
+
if (shouldAnalyze.toLowerCase() === 'y' || shouldAnalyze.toLowerCase() === 'yes') {
|
|
26
|
+
await this.analyzeProject();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// REPL 시작
|
|
30
|
+
await this.startREPL();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* 프로젝트 파일 분석
|
|
35
|
+
*/
|
|
36
|
+
async analyzeProject() {
|
|
37
|
+
console.log(chalk.cyan('\n📂 폴더 구조 분석 중...\n'));
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
this.projectFiles = await this.fileReader.listFiles();
|
|
41
|
+
|
|
42
|
+
if (this.projectFiles.length === 0) {
|
|
43
|
+
console.log(chalk.yellow('⚠️ 분석할 파일이 없습니다.'));
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// 폴더 트리 출력
|
|
48
|
+
console.log(chalk.bold('프로젝트 구조:'));
|
|
49
|
+
const tree = this.fileReader.generateTree(this.projectFiles);
|
|
50
|
+
console.log(tree);
|
|
51
|
+
|
|
52
|
+
console.log(chalk.green(`✅ 총 ${this.projectFiles.length}개 파일 발견\n`));
|
|
53
|
+
} catch (error) {
|
|
54
|
+
console.error(chalk.red(`❌ 파일 분석 실패: ${error.message}`));
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* REPL 모드 시작
|
|
60
|
+
*/
|
|
61
|
+
async startREPL() {
|
|
62
|
+
this.rl = readline.createInterface({
|
|
63
|
+
input: process.stdin,
|
|
64
|
+
output: process.stdout,
|
|
65
|
+
prompt: chalk.bold.cyan('> ')
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
console.log(chalk.gray('무엇을 도와드릴까요? (종료하려면 "exit" 입력)\n'));
|
|
69
|
+
this.rl.prompt();
|
|
70
|
+
|
|
71
|
+
this.rl.on('line', async (input) => {
|
|
72
|
+
const command = input.trim();
|
|
73
|
+
|
|
74
|
+
if (!command) {
|
|
75
|
+
this.rl.prompt();
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (command === 'exit' || command === 'quit') {
|
|
80
|
+
console.log(chalk.green('\n👋 안녕히 가세요!'));
|
|
81
|
+
this.rl.close();
|
|
82
|
+
process.exit(0);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
await this.handleCommand(command);
|
|
86
|
+
this.rl.prompt();
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
this.rl.on('close', () => {
|
|
90
|
+
process.exit(0);
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* 사용자 명령어 처리
|
|
96
|
+
*/
|
|
97
|
+
async handleCommand(command) {
|
|
98
|
+
const lowerCommand = command.toLowerCase();
|
|
99
|
+
|
|
100
|
+
// 파일 리뷰 요청 감지
|
|
101
|
+
if (lowerCommand.includes('리뷰') || lowerCommand.includes('review') ||
|
|
102
|
+
lowerCommand.includes('검사') || lowerCommand.includes('check')) {
|
|
103
|
+
await this.handleReviewRequest(command);
|
|
104
|
+
}
|
|
105
|
+
// 파일 목록 요청
|
|
106
|
+
else if (lowerCommand.includes('목록') || lowerCommand.includes('list') ||
|
|
107
|
+
lowerCommand.includes('파일')) {
|
|
108
|
+
this.showFileList();
|
|
109
|
+
}
|
|
110
|
+
// 도움말
|
|
111
|
+
else if (lowerCommand.includes('help') || lowerCommand.includes('도움말')) {
|
|
112
|
+
this.showHelp();
|
|
113
|
+
}
|
|
114
|
+
// 기타 - 일반 질문으로 처리
|
|
115
|
+
else {
|
|
116
|
+
console.log(chalk.yellow('\n💡 파일을 리뷰하려면 "파일명 리뷰해줘" 형식으로 입력해주세요.'));
|
|
117
|
+
console.log(chalk.gray(' 예: api.py 리뷰해줘, 모든 Python 파일 검사해줘\n'));
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* 리뷰 요청 처리
|
|
123
|
+
*/
|
|
124
|
+
async handleReviewRequest(command) {
|
|
125
|
+
// 파일 매칭
|
|
126
|
+
const matchedFiles = this.fileReader.matchFiles(command, this.projectFiles);
|
|
127
|
+
|
|
128
|
+
if (matchedFiles.length === 0) {
|
|
129
|
+
console.log(chalk.yellow('\n⚠️ 매칭되는 파일을 찾을 수 없습니다.'));
|
|
130
|
+
console.log(chalk.gray(' 사용 가능한 파일 목록을 보려면 "파일 목록" 입력\n'));
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
console.log(chalk.cyan(`\n🔍 ${matchedFiles.length}개 파일 리뷰 중...\n`));
|
|
135
|
+
|
|
136
|
+
for (const filePath of matchedFiles) {
|
|
137
|
+
await this.reviewFile(filePath);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* 단일 파일 리뷰
|
|
143
|
+
*/
|
|
144
|
+
async reviewFile(filePath) {
|
|
145
|
+
try {
|
|
146
|
+
// 파일 내용 읽기
|
|
147
|
+
const content = await this.fileReader.readFile(filePath);
|
|
148
|
+
|
|
149
|
+
// 언어 감지
|
|
150
|
+
const language = this.detectLanguage(filePath);
|
|
151
|
+
|
|
152
|
+
console.log(chalk.bold(`\n=== ${filePath} ===\n`));
|
|
153
|
+
|
|
154
|
+
// 서버에 리뷰 요청
|
|
155
|
+
const result = await this.client.reviewCode(content, language);
|
|
156
|
+
|
|
157
|
+
// 결과 출력
|
|
158
|
+
console.log(result.review);
|
|
159
|
+
console.log('');
|
|
160
|
+
|
|
161
|
+
} catch (error) {
|
|
162
|
+
console.error(chalk.red(`❌ 리뷰 실패 (${filePath}): ${error.message}\n`));
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* 파일 목록 출력
|
|
168
|
+
*/
|
|
169
|
+
showFileList() {
|
|
170
|
+
if (this.projectFiles.length === 0) {
|
|
171
|
+
console.log(chalk.yellow('\n⚠️ 분석된 파일이 없습니다. 먼저 프로젝트를 분석해주세요.\n'));
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
console.log(chalk.bold('\n📁 사용 가능한 파일:\n'));
|
|
176
|
+
this.projectFiles.forEach(file => {
|
|
177
|
+
console.log(chalk.gray(` - ${file}`));
|
|
178
|
+
});
|
|
179
|
+
console.log('');
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* 도움말 출력
|
|
184
|
+
*/
|
|
185
|
+
showHelp() {
|
|
186
|
+
console.log(chalk.bold('\n📖 사용 가능한 명령어:\n'));
|
|
187
|
+
console.log(chalk.cyan(' 파일명 리뷰해줘') + chalk.gray(' - 특정 파일 리뷰'));
|
|
188
|
+
console.log(chalk.cyan(' 모든 Python 파일 검사') + chalk.gray(' - 확장자별 리뷰'));
|
|
189
|
+
console.log(chalk.cyan(' 파일 목록') + chalk.gray(' - 분석 가능한 파일 목록'));
|
|
190
|
+
console.log(chalk.cyan(' help') + chalk.gray(' - 이 도움말'));
|
|
191
|
+
console.log(chalk.cyan(' exit') + chalk.gray(' - 종료\n'));
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* 파일 확장자로 언어 감지
|
|
196
|
+
*/
|
|
197
|
+
detectLanguage(filePath) {
|
|
198
|
+
const ext = filePath.split('.').pop().toLowerCase();
|
|
199
|
+
const langMap = {
|
|
200
|
+
'py': 'python',
|
|
201
|
+
'js': 'javascript',
|
|
202
|
+
'jsx': 'javascript',
|
|
203
|
+
'ts': 'typescript',
|
|
204
|
+
'tsx': 'typescript',
|
|
205
|
+
'java': 'java',
|
|
206
|
+
'cpp': 'cpp',
|
|
207
|
+
'c': 'c',
|
|
208
|
+
'go': 'go',
|
|
209
|
+
'rs': 'rust',
|
|
210
|
+
'rb': 'ruby',
|
|
211
|
+
'php': 'php'
|
|
212
|
+
};
|
|
213
|
+
return langMap[ext] || 'text';
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* 사용자에게 질문하고 답변 받기
|
|
218
|
+
*/
|
|
219
|
+
askQuestion(question) {
|
|
220
|
+
return new Promise((resolve) => {
|
|
221
|
+
const rl = readline.createInterface({
|
|
222
|
+
input: process.stdin,
|
|
223
|
+
output: process.stdout
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
rl.question(chalk.bold.cyan(question), (answer) => {
|
|
227
|
+
rl.close();
|
|
228
|
+
resolve(answer);
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
module.exports = ChatMode;
|
|
@@ -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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cciwon-code-review-cli",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"description": "AI-powered code review CLI tool using Qwen3-Coder-30B model with IP whitelist support",
|
|
5
5
|
"main": "lib/api-client.js",
|
|
6
6
|
"bin": {
|
|
@@ -26,7 +26,9 @@
|
|
|
26
26
|
"axios": "^1.6.0",
|
|
27
27
|
"commander": "^11.1.0",
|
|
28
28
|
"chalk": "^4.1.2",
|
|
29
|
-
"ora": "^5.4.1"
|
|
29
|
+
"ora": "^5.4.1",
|
|
30
|
+
"fast-glob": "^3.3.2",
|
|
31
|
+
"readline": "^1.3.0"
|
|
30
32
|
},
|
|
31
33
|
"engines": {
|
|
32
34
|
"node": ">=14.0.0"
|