cciwon-code-review-cli 1.0.1 → 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/bin/code-review.js +18 -0
- package/lib/chat-mode.js +234 -0
- package/lib/file-reader.js +159 -0
- package/package.json +4 -2
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/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"
|