jun-claude-code 0.6.0 → 0.6.2
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/dist/copy.js
CHANGED
|
@@ -40,25 +40,10 @@ exports.mergeSettingsJson = mergeSettingsJson;
|
|
|
40
40
|
exports.copyClaudeFiles = copyClaudeFiles;
|
|
41
41
|
const fs = __importStar(require("fs"));
|
|
42
42
|
const path = __importStar(require("path"));
|
|
43
|
-
const readline = __importStar(require("readline"));
|
|
44
43
|
const crypto = __importStar(require("crypto"));
|
|
45
44
|
const chalk_1 = __importDefault(require("chalk"));
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
*/
|
|
49
|
-
function askConfirmation(question) {
|
|
50
|
-
const rl = readline.createInterface({
|
|
51
|
-
input: process.stdin,
|
|
52
|
-
output: process.stdout,
|
|
53
|
-
});
|
|
54
|
-
return new Promise((resolve) => {
|
|
55
|
-
rl.question(question, (answer) => {
|
|
56
|
-
rl.close();
|
|
57
|
-
const normalized = answer.toLowerCase().trim();
|
|
58
|
-
resolve(normalized === 'y' || normalized === 'yes');
|
|
59
|
-
});
|
|
60
|
-
});
|
|
61
|
-
}
|
|
45
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
46
|
+
const { MultiSelect } = require('enquirer');
|
|
62
47
|
/**
|
|
63
48
|
* Calculate SHA-256 hash of a file
|
|
64
49
|
*/
|
|
@@ -119,6 +104,137 @@ function getDestClaudeDir() {
|
|
|
119
104
|
}
|
|
120
105
|
return path.join(homeDir, '.claude');
|
|
121
106
|
}
|
|
107
|
+
/**
|
|
108
|
+
* Categorize files into agents, skills (top-level dirs), and others
|
|
109
|
+
*/
|
|
110
|
+
function categorizeFiles(files) {
|
|
111
|
+
const agents = [];
|
|
112
|
+
const skillDirs = new Set();
|
|
113
|
+
const others = [];
|
|
114
|
+
for (const file of files) {
|
|
115
|
+
if (file.startsWith('agents/')) {
|
|
116
|
+
agents.push(file);
|
|
117
|
+
}
|
|
118
|
+
else if (file.startsWith('skills/')) {
|
|
119
|
+
const parts = file.split('/');
|
|
120
|
+
if (parts.length >= 2 && parts[1]) {
|
|
121
|
+
skillDirs.add(parts[1]);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
else {
|
|
125
|
+
others.push(file);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return {
|
|
129
|
+
agents: agents.sort(),
|
|
130
|
+
skills: Array.from(skillDirs).sort(),
|
|
131
|
+
others: others.sort(),
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Get file status (new/changed/unchanged)
|
|
136
|
+
*/
|
|
137
|
+
function getFileStatus(sourcePath, destPath) {
|
|
138
|
+
if (!fs.existsSync(destPath))
|
|
139
|
+
return 'new';
|
|
140
|
+
return getFileHash(sourcePath) === getFileHash(destPath) ? 'unchanged' : 'changed';
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Get skill directory status by checking all files within
|
|
144
|
+
*/
|
|
145
|
+
function getSkillStatus(skillName, sourceDir, destDir) {
|
|
146
|
+
const sourceSkillDir = path.join(sourceDir, 'skills', skillName);
|
|
147
|
+
const destSkillDir = path.join(destDir, 'skills', skillName);
|
|
148
|
+
if (!fs.existsSync(destSkillDir))
|
|
149
|
+
return 'new';
|
|
150
|
+
const sourceFiles = getAllFiles(sourceSkillDir, sourceSkillDir);
|
|
151
|
+
for (const file of sourceFiles) {
|
|
152
|
+
const src = path.join(sourceSkillDir, file);
|
|
153
|
+
const dst = path.join(destSkillDir, file);
|
|
154
|
+
if (!fs.existsSync(dst) || getFileHash(src) !== getFileHash(dst)) {
|
|
155
|
+
return 'changed';
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return 'unchanged';
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Format status label for display
|
|
162
|
+
*/
|
|
163
|
+
function statusLabel(status) {
|
|
164
|
+
switch (status) {
|
|
165
|
+
case 'new': return chalk_1.default.green('new');
|
|
166
|
+
case 'changed': return chalk_1.default.yellow('changed');
|
|
167
|
+
case 'unchanged': return chalk_1.default.gray('unchanged');
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Format status label with brackets for log output
|
|
172
|
+
*/
|
|
173
|
+
function statusBracket(status) {
|
|
174
|
+
switch (status) {
|
|
175
|
+
case 'new': return chalk_1.default.green('[new]');
|
|
176
|
+
case 'changed': return chalk_1.default.yellow('[changed]');
|
|
177
|
+
case 'unchanged': return chalk_1.default.gray('[unchanged]');
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Show MultiSelect prompt for a category
|
|
182
|
+
*/
|
|
183
|
+
async function selectItems(category, items) {
|
|
184
|
+
if (items.length === 0)
|
|
185
|
+
return [];
|
|
186
|
+
const choices = items.map(item => ({
|
|
187
|
+
name: item.name,
|
|
188
|
+
message: item.displayName,
|
|
189
|
+
hint: statusLabel(item.status),
|
|
190
|
+
enabled: item.status !== 'unchanged',
|
|
191
|
+
}));
|
|
192
|
+
const prompt = new MultiSelect({
|
|
193
|
+
name: category,
|
|
194
|
+
message: `Select ${category} to install`,
|
|
195
|
+
choices,
|
|
196
|
+
hint: '(↑↓ navigate, <space> toggle, <a> select all, <enter> confirm)',
|
|
197
|
+
});
|
|
198
|
+
try {
|
|
199
|
+
return await prompt.run();
|
|
200
|
+
}
|
|
201
|
+
catch {
|
|
202
|
+
console.log(chalk_1.default.yellow('\nInstallation cancelled.'));
|
|
203
|
+
process.exit(0);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Show MultiSelect prompt for skill sub-files across multiple skills.
|
|
208
|
+
* Groups files by skill with separators.
|
|
209
|
+
*/
|
|
210
|
+
async function selectSkillSubFiles(skills, sourceDir, destDir) {
|
|
211
|
+
const choices = [];
|
|
212
|
+
for (const { skillName, subFiles } of skills) {
|
|
213
|
+
choices.push({ role: 'separator', message: chalk_1.default.cyan(`── ${skillName} ──`) });
|
|
214
|
+
for (const file of subFiles) {
|
|
215
|
+
const status = getFileStatus(path.join(sourceDir, file), path.join(destDir, file));
|
|
216
|
+
choices.push({
|
|
217
|
+
name: file,
|
|
218
|
+
message: ` ${path.basename(file)}`,
|
|
219
|
+
hint: statusLabel(status),
|
|
220
|
+
enabled: status !== 'unchanged',
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
const prompt = new MultiSelect({
|
|
225
|
+
name: 'skill-files',
|
|
226
|
+
message: 'Select skill files to install',
|
|
227
|
+
choices,
|
|
228
|
+
hint: '(↑↓ navigate, <space> toggle, <a> select all, <enter> confirm)',
|
|
229
|
+
});
|
|
230
|
+
try {
|
|
231
|
+
return await prompt.run();
|
|
232
|
+
}
|
|
233
|
+
catch {
|
|
234
|
+
console.log(chalk_1.default.yellow('\nInstallation cancelled.'));
|
|
235
|
+
process.exit(0);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
122
238
|
/**
|
|
123
239
|
* Merge settings.json from source into destination.
|
|
124
240
|
* Hooks are merged per event key; duplicate hook entries (by deep equality) are skipped.
|
|
@@ -293,27 +409,37 @@ async function copyClaudeFiles(options = {}) {
|
|
|
293
409
|
console.log(chalk_1.default.yellow('No files found in templates/global directory'));
|
|
294
410
|
return;
|
|
295
411
|
}
|
|
296
|
-
|
|
412
|
+
const categorized = categorizeFiles(files);
|
|
413
|
+
console.log(chalk_1.default.cyan(`Found ${files.length} files (${categorized.agents.length} agents, ${categorized.skills.length} skills, ${categorized.others.length} others)`));
|
|
297
414
|
console.log();
|
|
298
|
-
// Dry run mode -
|
|
415
|
+
// Dry run mode - show categorized status
|
|
299
416
|
if (dryRun) {
|
|
300
417
|
console.log(chalk_1.default.yellow('[DRY RUN] Files that would be copied:'));
|
|
301
418
|
console.log();
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
const
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
const sourceHash = getFileHash(sourcePath);
|
|
308
|
-
const destHash = getFileHash(destPath);
|
|
309
|
-
const status = sourceHash === destHash ? chalk_1.default.gray('[unchanged]') : chalk_1.default.yellow('[overwrite]');
|
|
310
|
-
console.log(` ${status} ${file}`);
|
|
419
|
+
if (categorized.agents.length > 0) {
|
|
420
|
+
console.log(chalk_1.default.cyan(' Agents:'));
|
|
421
|
+
for (const file of categorized.agents) {
|
|
422
|
+
const status = getFileStatus(path.join(sourceDir, file), path.join(destDir, file));
|
|
423
|
+
console.log(` ${statusBracket(status)} ${path.basename(file, '.md')}`);
|
|
311
424
|
}
|
|
312
|
-
|
|
313
|
-
|
|
425
|
+
console.log();
|
|
426
|
+
}
|
|
427
|
+
if (categorized.skills.length > 0) {
|
|
428
|
+
console.log(chalk_1.default.cyan(' Skills:'));
|
|
429
|
+
for (const skill of categorized.skills) {
|
|
430
|
+
const status = getSkillStatus(skill, sourceDir, destDir);
|
|
431
|
+
console.log(` ${statusBracket(status)} ${skill}`);
|
|
432
|
+
}
|
|
433
|
+
console.log();
|
|
434
|
+
}
|
|
435
|
+
if (categorized.others.length > 0) {
|
|
436
|
+
console.log(chalk_1.default.cyan(' Others (auto-install):'));
|
|
437
|
+
for (const file of categorized.others) {
|
|
438
|
+
const status = getFileStatus(path.join(sourceDir, file), path.join(destDir, file));
|
|
439
|
+
console.log(` ${statusBracket(status)} ${file}`);
|
|
314
440
|
}
|
|
441
|
+
console.log();
|
|
315
442
|
}
|
|
316
|
-
// settings.json merge indicator
|
|
317
443
|
const sourceSettingsExists = fs.existsSync(path.join(sourceDir, 'settings.json'));
|
|
318
444
|
if (sourceSettingsExists) {
|
|
319
445
|
console.log(` ${chalk_1.default.blue('[merge]')} settings.json`);
|
|
@@ -322,34 +448,80 @@ async function copyClaudeFiles(options = {}) {
|
|
|
322
448
|
console.log(chalk_1.default.yellow('No files were copied (dry run mode)'));
|
|
323
449
|
return;
|
|
324
450
|
}
|
|
325
|
-
//
|
|
451
|
+
// Determine files to copy per category
|
|
452
|
+
let agentFiles = [];
|
|
453
|
+
let skillFiles = [];
|
|
454
|
+
let otherFiles = [];
|
|
455
|
+
if (force) {
|
|
456
|
+
agentFiles = categorized.agents;
|
|
457
|
+
skillFiles = files.filter(f => f.startsWith('skills/'));
|
|
458
|
+
otherFiles = categorized.others;
|
|
459
|
+
}
|
|
460
|
+
else {
|
|
461
|
+
// Others: auto-copy new/changed files
|
|
462
|
+
for (const file of categorized.others) {
|
|
463
|
+
const status = getFileStatus(path.join(sourceDir, file), path.join(destDir, file));
|
|
464
|
+
if (status !== 'unchanged') {
|
|
465
|
+
otherFiles.push(file);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
// Agents: MultiSelect
|
|
469
|
+
if (categorized.agents.length > 0) {
|
|
470
|
+
const agentItems = categorized.agents.map(file => ({
|
|
471
|
+
name: file,
|
|
472
|
+
displayName: path.basename(file, '.md'),
|
|
473
|
+
status: getFileStatus(path.join(sourceDir, file), path.join(destDir, file)),
|
|
474
|
+
}));
|
|
475
|
+
agentFiles = await selectItems('Agents', agentItems);
|
|
476
|
+
}
|
|
477
|
+
// Skills: MultiSelect (2-step)
|
|
478
|
+
if (categorized.skills.length > 0) {
|
|
479
|
+
const skillItems = categorized.skills.map(skill => ({
|
|
480
|
+
name: skill,
|
|
481
|
+
displayName: skill,
|
|
482
|
+
status: getSkillStatus(skill, sourceDir, destDir),
|
|
483
|
+
}));
|
|
484
|
+
const selectedSkills = await selectItems('Skills', skillItems);
|
|
485
|
+
// Step 2: 선택된 스킬의 하위 파일 선택
|
|
486
|
+
const multiFileSkills = [];
|
|
487
|
+
for (const skillName of selectedSkills) {
|
|
488
|
+
const skillSubFiles = files.filter(f => f.startsWith(`skills/${skillName}/`));
|
|
489
|
+
if (skillSubFiles.length > 1) {
|
|
490
|
+
multiFileSkills.push({ skillName, subFiles: skillSubFiles });
|
|
491
|
+
}
|
|
492
|
+
else {
|
|
493
|
+
// 파일이 1개뿐이면 자동 포함
|
|
494
|
+
skillFiles.push(...skillSubFiles);
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
// 하위 파일 선택이 필요한 스킬이 있으면 한 번의 프롬프트로 표시
|
|
498
|
+
if (multiFileSkills.length > 0) {
|
|
499
|
+
const selectedSubFiles = await selectSkillSubFiles(multiFileSkills, sourceDir, destDir);
|
|
500
|
+
skillFiles.push(...selectedSubFiles);
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
// Copy all selected files
|
|
505
|
+
const allFilesToCopy = [...otherFiles, ...agentFiles, ...skillFiles];
|
|
326
506
|
let copiedCount = 0;
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
const
|
|
330
|
-
|
|
331
|
-
const exists = fs.existsSync(destPath);
|
|
332
|
-
if (exists && !force) {
|
|
333
|
-
const sourceHash = getFileHash(sourcePath);
|
|
334
|
-
const destHash = getFileHash(destPath);
|
|
335
|
-
if (sourceHash === destHash) {
|
|
507
|
+
if (otherFiles.length > 0 || categorized.others.length > 0) {
|
|
508
|
+
// Show unchanged others for context
|
|
509
|
+
for (const file of categorized.others) {
|
|
510
|
+
if (!otherFiles.includes(file)) {
|
|
336
511
|
console.log(` ${chalk_1.default.gray('[unchanged]')} ${file}`);
|
|
337
|
-
skippedCount++;
|
|
338
|
-
continue;
|
|
339
|
-
}
|
|
340
|
-
// Hash differs - ask for confirmation
|
|
341
|
-
const shouldOverwrite = await askConfirmation(chalk_1.default.yellow(`File changed: ${file}. Overwrite? (y/N): `));
|
|
342
|
-
if (!shouldOverwrite) {
|
|
343
|
-
console.log(chalk_1.default.gray(` Skipped: ${file}`));
|
|
344
|
-
skippedCount++;
|
|
345
|
-
continue;
|
|
346
512
|
}
|
|
347
513
|
}
|
|
514
|
+
}
|
|
515
|
+
for (const file of allFilesToCopy) {
|
|
516
|
+
const sourcePath = path.join(sourceDir, file);
|
|
517
|
+
const destPath = path.join(destDir, file);
|
|
518
|
+
const exists = fs.existsSync(destPath);
|
|
348
519
|
copyFile(sourcePath, destPath);
|
|
349
|
-
const
|
|
350
|
-
console.log(` ${
|
|
520
|
+
const label = exists ? chalk_1.default.yellow('[overwritten]') : chalk_1.default.green('[created]');
|
|
521
|
+
console.log(` ${label} ${file}`);
|
|
351
522
|
copiedCount++;
|
|
352
523
|
}
|
|
524
|
+
const skippedCount = files.length - copiedCount;
|
|
353
525
|
// Merge settings.json (hooks are merged, not overwritten)
|
|
354
526
|
mergeSettingsJson(sourceDir, destDir, { project });
|
|
355
527
|
console.log();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jun-claude-code",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.2",
|
|
4
4
|
"description": "Claude Code configuration template - copy .claude settings to your project",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": "dist/cli.js",
|
|
@@ -16,7 +16,8 @@
|
|
|
16
16
|
"packageManager": "yarn@3.8.7",
|
|
17
17
|
"dependencies": {
|
|
18
18
|
"chalk": "^4.1.2",
|
|
19
|
-
"commander": "^11.1.0"
|
|
19
|
+
"commander": "^11.1.0",
|
|
20
|
+
"enquirer": "^2.4.1"
|
|
20
21
|
},
|
|
21
22
|
"devDependencies": {
|
|
22
23
|
"@types/node": "^20.10.0",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: code-reviewer
|
|
3
|
-
description: 코드
|
|
4
|
-
keywords: [코드리뷰, 체크리스트, lint, 규칙검증, 품질검사, Critical, Warning,
|
|
3
|
+
description: 코드 품질 검토 및 GitHub PR line-level comment 게시. CLAUDE.md/Skills 규칙 준수 확인, Critical/Warning 분류, lint 실행, PR 리뷰 코멘트 작성.
|
|
4
|
+
keywords: [코드리뷰, 체크리스트, lint, 규칙검증, 품질검사, Critical, Warning, 수정제안, PR리뷰, GitHub, comment, 라인코멘트]
|
|
5
5
|
model: opus
|
|
6
6
|
color: yellow
|
|
7
7
|
disallowedTools: [Edit, Write, NotebookEdit]
|
|
@@ -13,12 +13,13 @@ memory: project
|
|
|
13
13
|
|
|
14
14
|
<role>
|
|
15
15
|
|
|
16
|
-
작성된 코드가 프로젝트 규칙을 준수하는지
|
|
16
|
+
작성된 코드가 프로젝트 규칙을 준수하는지 검토하고, PR 리뷰 시 GitHub에 line-level comment를 게시하는 전문 Agent입니다.
|
|
17
17
|
|
|
18
18
|
1. **규칙 준수 확인**: CLAUDE.md, 프로젝트 체크리스트 기준 검토
|
|
19
19
|
2. **코드 품질 검사**: 가독성, 유지보수성, 일관성
|
|
20
20
|
3. **Lint 실행**: lint 실행 및 결과 확인
|
|
21
21
|
4. **개선 제안**: 발견된 문제에 대한 수정 방안 제시
|
|
22
|
+
5. **PR 리뷰 코멘트**: GitHub PR에 파일/라인별 review comment 게시
|
|
22
23
|
|
|
23
24
|
</role>
|
|
24
25
|
|
|
@@ -28,7 +29,16 @@ memory: project
|
|
|
28
29
|
|
|
29
30
|
## 검토 프로세스
|
|
30
31
|
|
|
31
|
-
### Step 1:
|
|
32
|
+
### Step 1: 리뷰 모드 판별
|
|
33
|
+
|
|
34
|
+
호출 시 전달받은 정보로 리뷰 모드를 결정합니다.
|
|
35
|
+
|
|
36
|
+
| 조건 | 모드 | 동작 |
|
|
37
|
+
|------|------|------|
|
|
38
|
+
| PR 번호가 주어짐 | **PR 리뷰 모드** | diff 분석 → 코드 검토 → GitHub PR에 line-level comment 게시 |
|
|
39
|
+
| PR 번호 없음 | **로컬 리뷰 모드** | 코드 검토 → 터미널에 결과 출력 |
|
|
40
|
+
|
|
41
|
+
### Step 2: 공통 체크리스트
|
|
32
42
|
|
|
33
43
|
| 항목 | 확인 |
|
|
34
44
|
|------|------|
|
|
@@ -39,11 +49,11 @@ memory: project
|
|
|
39
49
|
| 불필요한 코드 없음 | ☐ |
|
|
40
50
|
| 네이밍 컨벤션 준수 | ☐ |
|
|
41
51
|
|
|
42
|
-
### Step
|
|
52
|
+
### Step 3: 프로젝트별 체크리스트
|
|
43
53
|
|
|
44
54
|
프로젝트의 `.claude/skills/` 에 정의된 체크리스트 확인
|
|
45
55
|
|
|
46
|
-
### Step
|
|
56
|
+
### Step 4: Lint 실행
|
|
47
57
|
|
|
48
58
|
```bash
|
|
49
59
|
# 프로젝트에 맞는 lint 명령어 실행
|
|
@@ -52,6 +62,130 @@ npm run lint
|
|
|
52
62
|
yarn lint
|
|
53
63
|
```
|
|
54
64
|
|
|
65
|
+
### Step 5: PR 리뷰 코멘트 게시 (PR 리뷰 모드 전용)
|
|
66
|
+
|
|
67
|
+
PR 리뷰 모드에서는 발견된 이슈를 GitHub PR에 line-level review comment로 게시합니다.
|
|
68
|
+
|
|
69
|
+
#### 5-1. PR diff 수집
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
# PR의 변경된 파일과 diff 확인
|
|
73
|
+
gh pr diff {PR_NUMBER}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
#### 5-2. 이슈별 comment 데이터 구성
|
|
77
|
+
|
|
78
|
+
각 이슈를 아래 형식으로 수집합니다:
|
|
79
|
+
|
|
80
|
+
```json
|
|
81
|
+
{
|
|
82
|
+
"path": "src/example.ts",
|
|
83
|
+
"line": 42,
|
|
84
|
+
"body": "**[Critical]** 설명\n\n수정 방안: ..."
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
- `path`: 리포지토리 루트 기준 상대 경로
|
|
89
|
+
- `line`: diff에서 변경된 라인 번호 (새 파일 기준)
|
|
90
|
+
- `body`: Markdown 형식의 코멘트 본문
|
|
91
|
+
|
|
92
|
+
**여러 라인에 걸친 이슈**는 `start_line`과 `line`을 함께 사용합니다:
|
|
93
|
+
|
|
94
|
+
```json
|
|
95
|
+
{
|
|
96
|
+
"path": "src/example.ts",
|
|
97
|
+
"start_line": 10,
|
|
98
|
+
"line": 15,
|
|
99
|
+
"body": "**[Warning]** 설명"
|
|
100
|
+
}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
#### 5-3. comment body 작성 규칙
|
|
104
|
+
|
|
105
|
+
심각도에 따라 접두사를 붙입니다:
|
|
106
|
+
|
|
107
|
+
| 심각도 | 접두사 | 예시 |
|
|
108
|
+
|--------|--------|------|
|
|
109
|
+
| Critical | `🔴 **[Critical]**` | `🔴 **[Critical]** any 타입 사용 — 구체적 타입으로 변경 필요` |
|
|
110
|
+
| Warning | `🟡 **[Warning]**` | `🟡 **[Warning]** 매직 넘버 사용 — 상수로 추출 권장` |
|
|
111
|
+
| Info | `🔵 **[Info]**` | `🔵 **[Info]** Optional chaining으로 간소화 가능` |
|
|
112
|
+
|
|
113
|
+
본문 구성:
|
|
114
|
+
|
|
115
|
+
```markdown
|
|
116
|
+
{접두사} 이슈 제목
|
|
117
|
+
|
|
118
|
+
**문제**: 구체적인 문제 설명
|
|
119
|
+
**수정 방안**:
|
|
120
|
+
\`\`\`typescript
|
|
121
|
+
// 수정 예시 코드
|
|
122
|
+
\`\`\`
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
#### 5-4. GitHub PR Review Comment 게시
|
|
126
|
+
|
|
127
|
+
수집한 comment들을 개별 review comment로 게시합니다.
|
|
128
|
+
|
|
129
|
+
```bash
|
|
130
|
+
# owner/repo 확인
|
|
131
|
+
gh repo view --json nameWithOwner -q '.nameWithOwner'
|
|
132
|
+
|
|
133
|
+
# 최신 commit SHA 확인
|
|
134
|
+
gh pr view {PR_NUMBER} --json headRefOid -q '.headRefOid'
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
**단일 라인 comment 게시:**
|
|
138
|
+
|
|
139
|
+
```bash
|
|
140
|
+
gh api repos/{owner}/{repo}/pulls/{PR_NUMBER}/comments \
|
|
141
|
+
--method POST \
|
|
142
|
+
-f path="src/example.ts" \
|
|
143
|
+
-F line=42 \
|
|
144
|
+
-f side="RIGHT" \
|
|
145
|
+
-f commit_id="{COMMIT_SHA}" \
|
|
146
|
+
-f body="코멘트 내용"
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
**여러 라인 comment 게시:**
|
|
150
|
+
|
|
151
|
+
```bash
|
|
152
|
+
gh api repos/{owner}/{repo}/pulls/{PR_NUMBER}/comments \
|
|
153
|
+
--method POST \
|
|
154
|
+
-f path="src/example.ts" \
|
|
155
|
+
-F start_line=10 \
|
|
156
|
+
-F line=15 \
|
|
157
|
+
-f start_side="RIGHT" \
|
|
158
|
+
-f side="RIGHT" \
|
|
159
|
+
-f commit_id="{COMMIT_SHA}" \
|
|
160
|
+
-f body="코멘트 내용"
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
#### 5-5. 전체 요약 comment 게시
|
|
164
|
+
|
|
165
|
+
모든 line comment 게시 후, PR에 전체 요약을 일반 comment로 남깁니다.
|
|
166
|
+
|
|
167
|
+
```bash
|
|
168
|
+
gh pr comment {PR_NUMBER} --body "$(cat <<'EOF'
|
|
169
|
+
# Code Review 결과 요약
|
|
170
|
+
|
|
171
|
+
- **검토 파일**: N개
|
|
172
|
+
- **발견 이슈**: Critical N개, Warning N개, Info N개
|
|
173
|
+
- **전체 평가**: 통과 / 수정 필요 / 재작업 필요
|
|
174
|
+
|
|
175
|
+
## diff 범위 밖 이슈
|
|
176
|
+
- `path/to/file.ts:100` - 설명 (해당되는 경우만)
|
|
177
|
+
|
|
178
|
+
## 다음 단계
|
|
179
|
+
- ...
|
|
180
|
+
EOF
|
|
181
|
+
)"
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
#### 5-6. 주의사항
|
|
185
|
+
|
|
186
|
+
- **diff에 포함된 라인만 comment 가능**: 변경되지 않은 라인에는 comment를 달 수 없음 → diff 범위 밖 이슈는 요약 comment에 포함
|
|
187
|
+
- **이슈가 없으면 요약 comment만 게시**: line comment 없이 "이슈 없음" 요약만 남김
|
|
188
|
+
|
|
55
189
|
</instructions>
|
|
56
190
|
|
|
57
191
|
---
|
|
@@ -70,6 +204,8 @@ yarn lint
|
|
|
70
204
|
|
|
71
205
|
<output_format>
|
|
72
206
|
|
|
207
|
+
## 로컬 리뷰 모드 출력
|
|
208
|
+
|
|
73
209
|
```markdown
|
|
74
210
|
# Code Review 결과
|
|
75
211
|
|
|
@@ -116,6 +252,32 @@ yarn lint
|
|
|
116
252
|
- 이슈 없음: 최종 검증 진행
|
|
117
253
|
```
|
|
118
254
|
|
|
255
|
+
## PR 리뷰 모드 출력
|
|
256
|
+
|
|
257
|
+
```markdown
|
|
258
|
+
# PR Review 완료
|
|
259
|
+
|
|
260
|
+
## 1. 요약
|
|
261
|
+
- **PR**: #{PR_NUMBER}
|
|
262
|
+
- **검토 파일**: N개
|
|
263
|
+
- **게시된 comment**: N개 (Critical N, Warning N, Info N)
|
|
264
|
+
|
|
265
|
+
## 2. 게시된 comment 목록
|
|
266
|
+
|
|
267
|
+
| 파일 | 라인 | 심각도 | 이슈 |
|
|
268
|
+
|------|------|--------|------|
|
|
269
|
+
| `src/example.ts` | L42 | Critical | 설명 |
|
|
270
|
+
| `src/util.ts` | L10-15 | Warning | 설명 |
|
|
271
|
+
|
|
272
|
+
## 3. diff 범위 밖 이슈 (요약 comment에 포함)
|
|
273
|
+
- `path/to/file.ts:100` - 설명
|
|
274
|
+
|
|
275
|
+
## 4. Lint 결과
|
|
276
|
+
```
|
|
277
|
+
[lint 출력]
|
|
278
|
+
```
|
|
279
|
+
```
|
|
280
|
+
|
|
119
281
|
</output_format>
|
|
120
282
|
|
|
121
283
|
---
|
|
@@ -125,5 +287,6 @@ yarn lint
|
|
|
125
287
|
- **실제 문제만 지적**: 코드에 실질적 영향이 있는 부분만 리뷰
|
|
126
288
|
- **대안 제시 필수**: 문제 지적 시 해결책도 함께 제시
|
|
127
289
|
- **컨텍스트 고려**: 프로젝트 상황에 맞게 유연하게 판단
|
|
290
|
+
- **diff 범위 준수**: PR comment는 diff에 포함된 라인에만 게시 (범위 밖 이슈는 요약 comment에 기재)
|
|
128
291
|
|
|
129
292
|
</constraints>
|
|
@@ -144,13 +144,15 @@ grep -r "import.*변경된함수명" --include="*.ts" --include="*.tsx"
|
|
|
144
144
|
|
|
145
145
|
## 🔄 주요 변경사항
|
|
146
146
|
|
|
147
|
-
|
|
148
|
-
|
|
147
|
+
> 파일별이 아닌 **기능/목적 단위**로 묶어서 설명합니다.
|
|
148
|
+
|
|
149
|
+
### [기능/변경 목적 1]
|
|
149
150
|
- 변경 내용 설명
|
|
151
|
+
- 관련 파일: `file1.ts`, `file2.ts`
|
|
150
152
|
|
|
151
|
-
### [
|
|
152
|
-
**파일:** `path/to/file.ts`
|
|
153
|
+
### [기능/변경 목적 2]
|
|
153
154
|
- 변경 내용 설명
|
|
155
|
+
- 관련 파일: `file3.ts`
|
|
154
156
|
|
|
155
157
|
## ⚠️ 사이드 이펙트
|
|
156
158
|
|