sales-frontend-gemini-cli 0.3.0 → 0.4.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.
Files changed (77) hide show
  1. package/dist/common/helper.cjs +59 -8
  2. package/dist/common/helper.cjs.map +1 -1
  3. package/dist/common/helper.d.cts +15 -1
  4. package/dist/common/helper.d.ts +15 -1
  5. package/dist/common/helper.js +58 -9
  6. package/dist/common/helper.js.map +1 -1
  7. package/dist/{pr-review/gemini → etc}/installation-gcloud.cjs +1 -1
  8. package/dist/etc/installation-gcloud.cjs.map +1 -0
  9. package/dist/{pr-review/gemini → etc}/installation-gcloud.js +1 -1
  10. package/dist/etc/installation-gcloud.js.map +1 -0
  11. package/dist/{pr-review/gemini → etc}/interactive-version/index.cjs +2 -2
  12. package/dist/etc/interactive-version/index.cjs.map +1 -0
  13. package/dist/{pr-review/gemini → etc}/interactive-version/index.js +2 -2
  14. package/dist/etc/interactive-version/index.js.map +1 -0
  15. package/dist/{pr-review/gemini → etc}/login.cjs +1 -1
  16. package/dist/etc/login.cjs.map +1 -0
  17. package/dist/{pr-review/gemini → etc}/login.js +1 -1
  18. package/dist/etc/login.js.map +1 -0
  19. package/dist/{pr-review/gemini → etc}/vertex-version/index.cjs +1 -1
  20. package/dist/etc/vertex-version/index.cjs.map +1 -0
  21. package/dist/{pr-review/gemini → etc}/vertex-version/index.js +1 -1
  22. package/dist/etc/vertex-version/index.js.map +1 -0
  23. package/dist/pr-review/claude/claude-commander.cjs +31 -5
  24. package/dist/pr-review/claude/claude-commander.cjs.map +1 -1
  25. package/dist/pr-review/claude/claude-commander.js +31 -5
  26. package/dist/pr-review/claude/claude-commander.js.map +1 -1
  27. package/dist/pr-review/claude/installation-claude.cjs +38 -1
  28. package/dist/pr-review/claude/installation-claude.cjs.map +1 -1
  29. package/dist/pr-review/claude/installation-claude.js +33 -1
  30. package/dist/pr-review/claude/installation-claude.js.map +1 -1
  31. package/dist/pr-review/codex/codex-commander.cjs +88 -0
  32. package/dist/pr-review/codex/codex-commander.cjs.map +1 -0
  33. package/dist/pr-review/codex/codex-commander.d.cts +12 -0
  34. package/dist/pr-review/codex/codex-commander.d.ts +12 -0
  35. package/dist/pr-review/codex/codex-commander.js +80 -0
  36. package/dist/pr-review/codex/codex-commander.js.map +1 -0
  37. package/dist/pr-review/codex/installation-codex.cjs +62 -0
  38. package/dist/pr-review/codex/installation-codex.cjs.map +1 -0
  39. package/dist/pr-review/codex/installation-codex.d.cts +3 -0
  40. package/dist/pr-review/codex/installation-codex.d.ts +3 -0
  41. package/dist/pr-review/codex/installation-codex.js +55 -0
  42. package/dist/pr-review/codex/installation-codex.js.map +1 -0
  43. package/dist/pr-review/gemini/gemini-commander.cjs +28 -5
  44. package/dist/pr-review/gemini/gemini-commander.cjs.map +1 -1
  45. package/dist/pr-review/gemini/gemini-commander.js +28 -5
  46. package/dist/pr-review/gemini/gemini-commander.js.map +1 -1
  47. package/dist/pr-review/gemini/installation-gemini.cjs +35 -0
  48. package/dist/pr-review/gemini/installation-gemini.cjs.map +1 -1
  49. package/dist/pr-review/gemini/installation-gemini.js +30 -0
  50. package/dist/pr-review/gemini/installation-gemini.js.map +1 -1
  51. package/dist/pr-review/review-one-by-one.cjs +265 -36
  52. package/dist/pr-review/review-one-by-one.cjs.map +1 -1
  53. package/dist/pr-review/review-one-by-one.js +264 -35
  54. package/dist/pr-review/review-one-by-one.js.map +1 -1
  55. package/dist/pr-review/review.cjs +233 -19
  56. package/dist/pr-review/review.cjs.map +1 -1
  57. package/dist/pr-review/review.js +233 -19
  58. package/dist/pr-review/review.js.map +1 -1
  59. package/package.json +1 -1
  60. package/src/common/form/review-form.md +1 -1
  61. package/src/common/rules/review-rules.md +2 -0
  62. package/dist/pr-review/gemini/installation-gcloud.cjs.map +0 -1
  63. package/dist/pr-review/gemini/installation-gcloud.js.map +0 -1
  64. package/dist/pr-review/gemini/interactive-version/index.cjs.map +0 -1
  65. package/dist/pr-review/gemini/interactive-version/index.js.map +0 -1
  66. package/dist/pr-review/gemini/login.cjs.map +0 -1
  67. package/dist/pr-review/gemini/login.js.map +0 -1
  68. package/dist/pr-review/gemini/vertex-version/index.cjs.map +0 -1
  69. package/dist/pr-review/gemini/vertex-version/index.js.map +0 -1
  70. /package/dist/{pr-review/gemini → etc}/installation-gcloud.d.cts +0 -0
  71. /package/dist/{pr-review/gemini → etc}/installation-gcloud.d.ts +0 -0
  72. /package/dist/{pr-review/gemini → etc}/interactive-version/index.d.cts +0 -0
  73. /package/dist/{pr-review/gemini → etc}/interactive-version/index.d.ts +0 -0
  74. /package/dist/{pr-review/gemini → etc}/login.d.cts +0 -0
  75. /package/dist/{pr-review/gemini → etc}/login.d.ts +0 -0
  76. /package/dist/{pr-review/gemini → etc}/vertex-version/index.d.cts +0 -0
  77. /package/dist/{pr-review/gemini → etc}/vertex-version/index.d.ts +0 -0
@@ -0,0 +1,88 @@
1
+ 'use strict';
2
+
3
+ var fs = require('fs');
4
+ var path = require('path');
5
+ var url = require('url');
6
+
7
+ var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
8
+ function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
9
+
10
+ var fs__default = /*#__PURE__*/_interopDefault(fs);
11
+ var path__default = /*#__PURE__*/_interopDefault(path);
12
+
13
+ // src/pr-review/codex/codex-commander.ts
14
+ var __dirname$1 = path__default.default.dirname(url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('codex-commander.cjs', document.baseURI).href))));
15
+ var rulesPath = path__default.default.resolve(__dirname$1, "../../src/common/rules/review-rules.md");
16
+ var namingRulesPath = path__default.default.resolve(__dirname$1, "../../src/common/rules/naming-rule.md");
17
+ var codingConventionRulesPath = path__default.default.resolve(__dirname$1, "../../src/common/rules/coding-convention.md");
18
+ path__default.default.resolve(__dirname$1, "../../src/common/form/review-form.md");
19
+ path__default.default.resolve(__dirname$1, "../../src/common/form/review-form-one-by-one.md");
20
+ function isTestMode(args2 = process.argv.slice(2)) {
21
+ return args2.includes("--test");
22
+ }
23
+ function createTraceLogger(scope, args2 = process.argv.slice(2)) {
24
+ const enabled = isTestMode(args2);
25
+ return (step, detail) => {
26
+ if (!enabled) {
27
+ return;
28
+ }
29
+ console.log(`[TRACE][${scope}] ${step}${detail ? ` | ${detail}` : ""}`);
30
+ };
31
+ }
32
+
33
+ // src/pr-review/codex/codex-commander.ts
34
+ var args = process.argv.slice(2);
35
+ var trace = createTraceLogger("codex-commander", args);
36
+ var createCodexCommand = (tempDiffPath, reviewFormPath2) => {
37
+ trace("createCodexCommand:start", `tempDiffPath=${tempDiffPath}, reviewFormPath=${reviewFormPath2}`);
38
+ let modelOption = "";
39
+ if (args.includes("--review")) {
40
+ modelOption = "--model gpt-5";
41
+ trace("model:review", modelOption);
42
+ } else if (args.includes("--flash")) {
43
+ modelOption = "--model gpt-5-mini";
44
+ trace("model:flash", modelOption);
45
+ } else {
46
+ const modelIndex = args.indexOf("--model");
47
+ if (modelIndex !== -1 && args[modelIndex + 1]) {
48
+ console.warn("\u26A0\uFE0F \uC9C0\uC815\uD55C \uBAA8\uB378\uC774 \uC5C6\uB294 \uACBD\uC6B0, \uC5D0\uB7EC\uAC00 \uBC1C\uC0DD\uD558\uB2C8 \uC8FC\uC758\uD558\uC138\uC694.");
49
+ modelOption = `--model ${args[modelIndex + 1]}`;
50
+ trace("model:custom", modelOption);
51
+ } else {
52
+ console.warn("\u26A0\uFE0F \uBAA8\uB378\uC774 \uC9C0\uC815\uB418\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4. \uAE30\uBCF8 \uBAA8\uB378\uC778 gpt-5-mini\uB97C \uC0AC\uC6A9\uD569\uB2C8\uB2E4.");
53
+ modelOption = "--model gpt-5-mini";
54
+ trace("model:default", modelOption);
55
+ }
56
+ }
57
+ const rules = [rulesPath, namingRulesPath, codingConventionRulesPath].filter((filePath) => fs__default.default.existsSync(filePath)).map((filePath) => `- ${filePath}`).join("\n");
58
+ const rulesCount = rules ? rules.split("\n").length : 0;
59
+ trace("rules:loaded", `count=${rulesCount}`);
60
+ const hasReviewForm = fs__default.default.existsSync(reviewFormPath2);
61
+ const reviewFormLine = hasReviewForm ? `- ${reviewFormPath2}` : "";
62
+ trace("reviewForm:status", reviewFormLine ? "exists" : "missing");
63
+ const prompt = `\uC544\uB798 \uD30C\uC77C\uB4E4\uC744 \uCC38\uACE0\uD574\uC11C \uCF54\uB4DC \uB9AC\uBDF0\uB97C \uC9C4\uD589\uD574\uC918.
64
+ \uADDC\uCE59 \uD30C\uC77C:
65
+ ${rules || "- (\uC5C6\uC74C)"}
66
+ \uB9AC\uBDF0 \uC591\uC2DD \uD30C\uC77C:
67
+ ${reviewFormLine || "- (\uC5C6\uC74C)"}
68
+ \uB9AC\uBDF0 \uB300\uC0C1 diff \uD30C\uC77C:
69
+ - ${tempDiffPath}
70
+
71
+ \uBC18\uB4DC\uC2DC \uB9AC\uBDF0 \uC591\uC2DD\uC5D0 \uB9DE\uCDB0 \uC791\uC131\uD574\uC918.`;
72
+ const command = `codex exec ${modelOption} "${prompt.replace(/"/g, '\\"')}"`;
73
+ trace("command:created");
74
+ if (args.includes("--test")) {
75
+ const safeCommand = command.replace(/"/g, '\\"');
76
+ trace("test-mode:return-preview");
77
+ return `echo "[TEST MODE] Codex \uBA85\uB839\uC5B4\uAC00 \uC2E4\uD589\uB418\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4.
78
+
79
+ \uC0DD\uC131\uB420 \uBA85\uB839\uC5B4 \uBBF8\uB9AC\uBCF4\uAE30:
80
+ ${safeCommand}"`;
81
+ }
82
+ trace("createCodexCommand:end");
83
+ return command;
84
+ };
85
+
86
+ exports.createCodexCommand = createCodexCommand;
87
+ //# sourceMappingURL=codex-commander.cjs.map
88
+ //# sourceMappingURL=codex-commander.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/common/helper.ts","../../../src/pr-review/codex/codex-commander.ts"],"names":["__dirname","path","fileURLToPath","args","reviewFormPath","fs"],"mappings":"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","file":"codex-commander.cjs","sourcesContent":["import { execSync } from 'child_process';\nimport fs from 'fs';\nimport path from 'path';\nimport readline from 'readline';\nimport { fileURLToPath } from 'url';\n\nimport { AIServiceType } from './types';\n\nconst __dirname = path.dirname(fileURLToPath(import.meta.url));\n\n// 설치된 위치에 맞게 규칙/양식 파일 경로를 계산 (dist에서 src로 이동 등 고려)\n// dist/common/helper.js 가 실행되므로 __dirname은 .../dist/common 입니다.\n// 따라서 ../../src/common/rules 로 이동해야 원본 소스의 규칙 파일을 찾을 수 있습니다.\nexport const rulesPath = path.resolve(__dirname, '../../src/common/rules/review-rules.md');\nexport const namingRulesPath = path.resolve(__dirname, '../../src/common/rules/naming-rule.md');\nexport const codingConventionRulesPath = path.resolve(__dirname, '../../src/common/rules/coding-convention.md');\nexport const reviewFormPath = path.resolve(__dirname, '../../src/common/form/review-form.md');\nexport const reviewFormOneByOnePath = path.resolve(__dirname, '../../src/common/form/review-form-one-by-one.md');\nexport const REPORT_DIR = '.review-report';\nexport const tempDiffPath = 'temp_diff.txt';\nexport const AIServices: AIServiceType[] = ['gemini', 'claude', 'codex'];\nexport const ignoreList = [\n 'package.json',\n '*.yml',\n '*.md',\n '*.lock',\n 'dist/',\n 'node_modules/',\n 'assets/',\n 'public/',\n '*.json',\n '*.yaml',\n '.review-report/' // 생성되는 리포트 폴더도 제외\n];\n\nexport function isTestMode(args: string[] = process.argv.slice(2)) {\n return args.includes('--test');\n}\n\nexport function createTraceLogger(scope: string, args: string[] = process.argv.slice(2)) {\n const enabled = isTestMode(args);\n\n return (step: string, detail?: string) => {\n if (!enabled) {\n return;\n }\n\n console.log(`[TRACE][${scope}] ${step}${detail ? ` | ${detail}` : ''}`);\n };\n}\n\nexport function getNextFilePath(dir: string, baseName: string, extension: string) {\n let counter = 1;\n // eslint-disable-next-line no-constant-condition\n while (true) {\n const filePath = path.join(dir, `${baseName}-${counter}${extension}`);\n if (!fs.existsSync(filePath)) {\n return filePath;\n }\n counter++;\n }\n}\n\nexport function deleteFile(filePath: string) {\n if (fs.existsSync(filePath)) {\n fs.unlinkSync(filePath);\n }\n}\n\n/**\n * 임시파일 삭제\n */\nexport function deleteTempDiff() {\n deleteFile(tempDiffPath);\n}\n\n/**\n * 리뷰 결과 폴더 생성\n */\nexport function createReportDirectory() {\n if (!fs.existsSync(REPORT_DIR)) {\n fs.mkdirSync(REPORT_DIR, { recursive: true });\n }\n}\n\n/**\n * 현재 시간 문자열 생성\n */\nexport function getNowString() {\n const now = new Date();\n const YYYY = now.getFullYear();\n const MM = String(now.getMonth() + 1).padStart(2, '0');\n const DD = String(now.getDate()).padStart(2, '0');\n const HH = String(now.getHours()).padStart(2, '0');\n const mm = String(now.getMinutes()).padStart(2, '0');\n const ss = String(now.getSeconds()).padStart(2, '0');\n\n return `${YYYY}-${MM}-${DD}_${HH}-${mm}-${ss}`;\n}\n\nexport function getGitDiffFilter() {\n // 1. 리뷰 대상 파일 확장자 정의\n const includeExtensions = ['*.ts', '*.tsx', '*.js', '*.jsx'];\n\n // ignoreList 를 import 하여 재사용하여 작성한다.\n const excludePatterns = ignoreList.map((item) => `:(exclude)${item}`);\n\n // const excludePatterns = [':(exclude)*.lock', ':(exclude)dist/', ':(exclude)*.md'];\n\n // 2. 변경된 파일 목록 가져오기 (각 패턴을 따옴표로 감싸서 쉘 에러 방지)\n const quote = (pattern: string) => `\"${pattern}\"`;\n const includeParams = includeExtensions.map(quote).join(' ');\n const excludeParams = excludePatterns.map(quote).join(' ');\n\n return { includeParams, excludeParams };\n}\n\n/**\n * openReport를 OS별로 동작하도록 변경\n * 우선순위:\n * 1. Chrome 시도\n * - macOS: open -a \"Google Chrome\" \"<path>\"\n * - Ubuntu/Linux: google-chrome \"<path>\"\n * 2. 실패 시 기본 브라우저로 폴백\n * - macOS: open \"<path>\"\n * - Ubuntu/Linux: xdg-open \"<path>\"\n * 3. 둘 다 실패하면 에러 출력\n * 4. 미지원 플랫폼이면 플랫폼 경고 출력\n */\nexport function openReport(reportPath: string) {\n const resolvedPath = path.resolve(reportPath);\n const { platform } = process;\n\n const openWithChrome = () => {\n if (platform === 'darwin') {\n execSync(`open -a \"Google Chrome\" \"${resolvedPath}\"`, { stdio: 'ignore' });\n\n return true;\n }\n\n if (platform === 'linux') {\n execSync(`google-chrome \"${resolvedPath}\"`, { stdio: 'ignore' });\n\n return true;\n }\n\n return false;\n };\n\n const openWithDefaultBrowser = () => {\n if (platform === 'darwin') {\n execSync(`open \"${resolvedPath}\"`, { stdio: 'ignore' });\n\n return true;\n }\n\n if (platform === 'linux') {\n execSync(`xdg-open \"${resolvedPath}\"`, { stdio: 'ignore' });\n\n return true;\n }\n\n return false;\n };\n\n try {\n if (openWithChrome()) {\n console.log('🚀 Google Chrome에서 리포트를 열었습니다.');\n\n return;\n }\n } catch {\n // Chrome 실행 실패 시 기본 브라우저로 폴백\n }\n\n try {\n if (openWithDefaultBrowser()) {\n console.log('🚀 기본 브라우저에서 리포트를 열었습니다.');\n\n return;\n }\n } catch (e) {\n console.error('⚠️ 브라우저 열기 실패:', e);\n\n return;\n }\n\n console.error(`⚠️ 지원하지 않는 플랫폼입니다: ${platform}`);\n}\n\nexport function getDiffArgs() {\n const args = process.argv.slice(2);\n const commitIndex = args.indexOf('--commit');\n const { includeParams, excludeParams } = getGitDiffFilter();\n\n let diffArgs = '';\n\n if (commitIndex !== -1) {\n // 특정 커밋 (및 이전 n개) 리뷰\n const commitHash = args[commitIndex + 1];\n if (!commitHash) {\n console.error('❌ 커밋 해시가 제공되지 않았습니다.');\n process.exit(1);\n }\n\n // n값 확인 (optional)\n const nextArg = args[commitIndex + 2];\n let n = 0;\n if (nextArg && !nextArg.startsWith('--')) {\n n = parseInt(nextArg, 10);\n if (isNaN(n)) {\n n = 0;\n }\n }\n\n console.log(`ℹ️ 커밋 '${commitHash}' ${n > 0 ? ` 포함 총 ${n + 1}개의 커밋` : ''}을 리뷰합니다...`);\n diffArgs = `${commitHash}~${n + 1} ${commitHash}`;\n } else {\n // 기본 모드:\n // 1. Unstaged 변경사항 확인\n try {\n const check = execSync(`git diff --name-only -- ${includeParams} ${excludeParams}`).toString();\n if (!check.trim()) {\n console.log('ℹ️ Unstaged 변경사항이 없습니다. 마지막 커밋(HEAD)을 리뷰합니다...');\n diffArgs = 'HEAD~1 HEAD';\n }\n } catch {\n // git diff 실패시 무시\n }\n }\n\n return diffArgs;\n}\n\n// export const ServiceType = {\n// GEMINI: 'gemini',\n// CLAUDE: 'claude',\n// CODEX: 'codex'\n\n// }\n\n/**\n * AI 서비스 선택\n */\nexport function selectAIService() {\n const args = process.argv.slice(2);\n const serviceIndex = args.indexOf('--service');\n const service = args[serviceIndex + 1];\n if (!service) {\n console.error('❌ 서비스가 선택되지 않았습니다.');\n process.exit(1);\n }\n\n return service;\n}\n\n/**\n * 터미널에서 라디오 버튼 형태로 AI 서비스를 선택합니다.\n */\nexport async function showSelectionAIService(): Promise<AIServiceType> {\n let selectedIndex = 0;\n\n // Use readline to handle keypresses\n // 키 입력을 처리하기 위해 readline 인터페이스 사용\n const rl = readline.createInterface({\n input: process.stdin,\n output: process.stdout,\n terminal: true\n });\n\n let firstRender = true;\n\n // Hide cursor\n process.stdout.write('\\u001b[?25l');\n\n const render = () => {\n if (!firstRender) {\n // Move cursor back to the starting line of the selection UI\n // We print (1 question line + services.length lines)\n // 선택 UI의 시작 라인으로 커서 이동 (질문 1줄 + 서비스 목록 N줄)\n readline.moveCursor(process.stdout, 0, -(AIServices.length + 1));\n }\n firstRender = false;\n\n // Clear everything from cursor down to avoid ghosting/overlaps\n // 잔상이나 겹침을 방지하기 위해 커서 위치부터 아래쪽 모두 지움\n readline.clearScreenDown(process.stdout);\n\n process.stdout.write(\n '🤖 AI 서비스를 선택해주세요 (\\u001b[33m↑↓ 방향키\\u001b[0m 이동, \\u001b[33mEnter\\u001b[0m 선택):\\n'\n );\n AIServices.forEach((service, index) => {\n if (index === selectedIndex) {\n process.stdout.write(` \\u001b[36m>\\u001b[0m \\u001b[36m◉\\u001b[0m \\u001b[1m${service}\\u001b[0m\\n`);\n } else {\n process.stdout.write(` ◯ ${service}\\n`);\n }\n });\n };\n\n render();\n\n return new Promise((resolve) => {\n const onData = (data: Buffer) => {\n const key = data.toString();\n if (key === '\\u0003') {\n // Ctrl+C\n process.stdout.write('\\u001b[?25h'); // Show cursor\n process.exit(0);\n }\n if (key === '\\x1b[A') {\n // Up arrow\n selectedIndex = (selectedIndex - 1 + AIServices.length) % AIServices.length;\n render();\n } else if (key === '\\x1b[B') {\n // Down arrow\n selectedIndex = (selectedIndex + 1) % AIServices.length;\n render();\n } else if (key === '\\r' || key === '\\n') {\n // Enter\n process.stdin.removeListener('data', onData);\n process.stdin.setRawMode(false);\n process.stdin.pause();\n rl.close();\n\n // Show cursor\n process.stdout.write('\\u001b[?25h');\n\n console.log(`\\n✅ \\u001b[32m${AIServices[selectedIndex]}\\u001b[0m 서비스가 선택되었습니다.\\n`);\n const result = AIServices[selectedIndex];\n if (result) {\n resolve(result);\n }\n }\n };\n\n process.stdin.setRawMode(true);\n process.stdin.resume();\n process.stdin.on('data', onData);\n });\n}\n","import fs from 'fs';\n\nimport { codingConventionRulesPath, createTraceLogger, namingRulesPath, rulesPath } from '../../common/helper';\n\nconst args = process.argv.slice(2);\nconst trace = createTraceLogger('codex-commander', args);\n\n/**\n * @description\n * Codex CLI를 활용한 코드 리뷰 명령어를 생성합니다.\n *\n * 모델 기본값:\n * - --review: gpt-5 (정밀 리뷰)\n * - --flash: gpt-5-mini (빠른 리뷰)\n * - 미지정: gpt-5-mini\n */\nexport const createCodexCommand = (tempDiffPath: string, reviewFormPath: string) => {\n trace('createCodexCommand:start', `tempDiffPath=${tempDiffPath}, reviewFormPath=${reviewFormPath}`);\n let modelOption = '';\n\n if (args.includes('--review')) {\n modelOption = '--model gpt-5';\n trace('model:review', modelOption);\n } else if (args.includes('--flash')) {\n modelOption = '--model gpt-5-mini';\n trace('model:flash', modelOption);\n } else {\n const modelIndex = args.indexOf('--model');\n if (modelIndex !== -1 && args[modelIndex + 1]) {\n console.warn('⚠️ 지정한 모델이 없는 경우, 에러가 발생하니 주의하세요.');\n modelOption = `--model ${args[modelIndex + 1]}`;\n trace('model:custom', modelOption);\n } else {\n console.warn('⚠️ 모델이 지정되지 않았습니다. 기본 모델인 gpt-5-mini를 사용합니다.');\n modelOption = '--model gpt-5-mini';\n trace('model:default', modelOption);\n }\n }\n\n const rules = [rulesPath, namingRulesPath, codingConventionRulesPath]\n .filter((filePath) => fs.existsSync(filePath))\n .map((filePath) => `- ${filePath}`)\n .join('\\n');\n const rulesCount = rules ? rules.split('\\n').length : 0;\n trace('rules:loaded', `count=${rulesCount}`);\n\n const hasReviewForm = fs.existsSync(reviewFormPath);\n const reviewFormLine = hasReviewForm ? `- ${reviewFormPath}` : '';\n trace('reviewForm:status', reviewFormLine ? 'exists' : 'missing');\n\n const prompt = `아래 파일들을 참고해서 코드 리뷰를 진행해줘.\n규칙 파일: \n${rules || '- (없음)'}\n리뷰 양식 파일:\n${reviewFormLine || '- (없음)'}\n리뷰 대상 diff 파일:\n- ${tempDiffPath}\n\n반드시 리뷰 양식에 맞춰 작성해줘.`;\n\n const command = `codex exec ${modelOption} \"${prompt.replace(/\"/g, '\\\\\"')}\"`;\n trace('command:created');\n\n if (args.includes('--test')) {\n const safeCommand = command.replace(/\"/g, '\\\\\"');\n trace('test-mode:return-preview');\n\n return `echo \"[TEST MODE] Codex 명령어가 실행되지 않았습니다.\\n\\n생성될 명령어 미리보기:\\n${safeCommand}\"`;\n }\n\n trace('createCodexCommand:end');\n\n return command;\n};\n"]}
@@ -0,0 +1,12 @@
1
+ /**
2
+ * @description
3
+ * Codex CLI를 활용한 코드 리뷰 명령어를 생성합니다.
4
+ *
5
+ * 모델 기본값:
6
+ * - --review: gpt-5 (정밀 리뷰)
7
+ * - --flash: gpt-5-mini (빠른 리뷰)
8
+ * - 미지정: gpt-5-mini
9
+ */
10
+ declare const createCodexCommand: (tempDiffPath: string, reviewFormPath: string) => string;
11
+
12
+ export { createCodexCommand };
@@ -0,0 +1,12 @@
1
+ /**
2
+ * @description
3
+ * Codex CLI를 활용한 코드 리뷰 명령어를 생성합니다.
4
+ *
5
+ * 모델 기본값:
6
+ * - --review: gpt-5 (정밀 리뷰)
7
+ * - --flash: gpt-5-mini (빠른 리뷰)
8
+ * - 미지정: gpt-5-mini
9
+ */
10
+ declare const createCodexCommand: (tempDiffPath: string, reviewFormPath: string) => string;
11
+
12
+ export { createCodexCommand };
@@ -0,0 +1,80 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { fileURLToPath } from 'url';
4
+
5
+ // src/pr-review/codex/codex-commander.ts
6
+ var __dirname = path.dirname(fileURLToPath(import.meta.url));
7
+ var rulesPath = path.resolve(__dirname, "../../src/common/rules/review-rules.md");
8
+ var namingRulesPath = path.resolve(__dirname, "../../src/common/rules/naming-rule.md");
9
+ var codingConventionRulesPath = path.resolve(__dirname, "../../src/common/rules/coding-convention.md");
10
+ path.resolve(__dirname, "../../src/common/form/review-form.md");
11
+ path.resolve(__dirname, "../../src/common/form/review-form-one-by-one.md");
12
+ function isTestMode(args2 = process.argv.slice(2)) {
13
+ return args2.includes("--test");
14
+ }
15
+ function createTraceLogger(scope, args2 = process.argv.slice(2)) {
16
+ const enabled = isTestMode(args2);
17
+ return (step, detail) => {
18
+ if (!enabled) {
19
+ return;
20
+ }
21
+ console.log(`[TRACE][${scope}] ${step}${detail ? ` | ${detail}` : ""}`);
22
+ };
23
+ }
24
+
25
+ // src/pr-review/codex/codex-commander.ts
26
+ var args = process.argv.slice(2);
27
+ var trace = createTraceLogger("codex-commander", args);
28
+ var createCodexCommand = (tempDiffPath, reviewFormPath2) => {
29
+ trace("createCodexCommand:start", `tempDiffPath=${tempDiffPath}, reviewFormPath=${reviewFormPath2}`);
30
+ let modelOption = "";
31
+ if (args.includes("--review")) {
32
+ modelOption = "--model gpt-5";
33
+ trace("model:review", modelOption);
34
+ } else if (args.includes("--flash")) {
35
+ modelOption = "--model gpt-5-mini";
36
+ trace("model:flash", modelOption);
37
+ } else {
38
+ const modelIndex = args.indexOf("--model");
39
+ if (modelIndex !== -1 && args[modelIndex + 1]) {
40
+ console.warn("\u26A0\uFE0F \uC9C0\uC815\uD55C \uBAA8\uB378\uC774 \uC5C6\uB294 \uACBD\uC6B0, \uC5D0\uB7EC\uAC00 \uBC1C\uC0DD\uD558\uB2C8 \uC8FC\uC758\uD558\uC138\uC694.");
41
+ modelOption = `--model ${args[modelIndex + 1]}`;
42
+ trace("model:custom", modelOption);
43
+ } else {
44
+ console.warn("\u26A0\uFE0F \uBAA8\uB378\uC774 \uC9C0\uC815\uB418\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4. \uAE30\uBCF8 \uBAA8\uB378\uC778 gpt-5-mini\uB97C \uC0AC\uC6A9\uD569\uB2C8\uB2E4.");
45
+ modelOption = "--model gpt-5-mini";
46
+ trace("model:default", modelOption);
47
+ }
48
+ }
49
+ const rules = [rulesPath, namingRulesPath, codingConventionRulesPath].filter((filePath) => fs.existsSync(filePath)).map((filePath) => `- ${filePath}`).join("\n");
50
+ const rulesCount = rules ? rules.split("\n").length : 0;
51
+ trace("rules:loaded", `count=${rulesCount}`);
52
+ const hasReviewForm = fs.existsSync(reviewFormPath2);
53
+ const reviewFormLine = hasReviewForm ? `- ${reviewFormPath2}` : "";
54
+ trace("reviewForm:status", reviewFormLine ? "exists" : "missing");
55
+ const prompt = `\uC544\uB798 \uD30C\uC77C\uB4E4\uC744 \uCC38\uACE0\uD574\uC11C \uCF54\uB4DC \uB9AC\uBDF0\uB97C \uC9C4\uD589\uD574\uC918.
56
+ \uADDC\uCE59 \uD30C\uC77C:
57
+ ${rules || "- (\uC5C6\uC74C)"}
58
+ \uB9AC\uBDF0 \uC591\uC2DD \uD30C\uC77C:
59
+ ${reviewFormLine || "- (\uC5C6\uC74C)"}
60
+ \uB9AC\uBDF0 \uB300\uC0C1 diff \uD30C\uC77C:
61
+ - ${tempDiffPath}
62
+
63
+ \uBC18\uB4DC\uC2DC \uB9AC\uBDF0 \uC591\uC2DD\uC5D0 \uB9DE\uCDB0 \uC791\uC131\uD574\uC918.`;
64
+ const command = `codex exec ${modelOption} "${prompt.replace(/"/g, '\\"')}"`;
65
+ trace("command:created");
66
+ if (args.includes("--test")) {
67
+ const safeCommand = command.replace(/"/g, '\\"');
68
+ trace("test-mode:return-preview");
69
+ return `echo "[TEST MODE] Codex \uBA85\uB839\uC5B4\uAC00 \uC2E4\uD589\uB418\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4.
70
+
71
+ \uC0DD\uC131\uB420 \uBA85\uB839\uC5B4 \uBBF8\uB9AC\uBCF4\uAE30:
72
+ ${safeCommand}"`;
73
+ }
74
+ trace("createCodexCommand:end");
75
+ return command;
76
+ };
77
+
78
+ export { createCodexCommand };
79
+ //# sourceMappingURL=codex-commander.js.map
80
+ //# sourceMappingURL=codex-commander.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/common/helper.ts","../../../src/pr-review/codex/codex-commander.ts"],"names":["args","reviewFormPath"],"mappings":"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","file":"codex-commander.js","sourcesContent":["import { execSync } from 'child_process';\nimport fs from 'fs';\nimport path from 'path';\nimport readline from 'readline';\nimport { fileURLToPath } from 'url';\n\nimport { AIServiceType } from './types';\n\nconst __dirname = path.dirname(fileURLToPath(import.meta.url));\n\n// 설치된 위치에 맞게 규칙/양식 파일 경로를 계산 (dist에서 src로 이동 등 고려)\n// dist/common/helper.js 가 실행되므로 __dirname은 .../dist/common 입니다.\n// 따라서 ../../src/common/rules 로 이동해야 원본 소스의 규칙 파일을 찾을 수 있습니다.\nexport const rulesPath = path.resolve(__dirname, '../../src/common/rules/review-rules.md');\nexport const namingRulesPath = path.resolve(__dirname, '../../src/common/rules/naming-rule.md');\nexport const codingConventionRulesPath = path.resolve(__dirname, '../../src/common/rules/coding-convention.md');\nexport const reviewFormPath = path.resolve(__dirname, '../../src/common/form/review-form.md');\nexport const reviewFormOneByOnePath = path.resolve(__dirname, '../../src/common/form/review-form-one-by-one.md');\nexport const REPORT_DIR = '.review-report';\nexport const tempDiffPath = 'temp_diff.txt';\nexport const AIServices: AIServiceType[] = ['gemini', 'claude', 'codex'];\nexport const ignoreList = [\n 'package.json',\n '*.yml',\n '*.md',\n '*.lock',\n 'dist/',\n 'node_modules/',\n 'assets/',\n 'public/',\n '*.json',\n '*.yaml',\n '.review-report/' // 생성되는 리포트 폴더도 제외\n];\n\nexport function isTestMode(args: string[] = process.argv.slice(2)) {\n return args.includes('--test');\n}\n\nexport function createTraceLogger(scope: string, args: string[] = process.argv.slice(2)) {\n const enabled = isTestMode(args);\n\n return (step: string, detail?: string) => {\n if (!enabled) {\n return;\n }\n\n console.log(`[TRACE][${scope}] ${step}${detail ? ` | ${detail}` : ''}`);\n };\n}\n\nexport function getNextFilePath(dir: string, baseName: string, extension: string) {\n let counter = 1;\n // eslint-disable-next-line no-constant-condition\n while (true) {\n const filePath = path.join(dir, `${baseName}-${counter}${extension}`);\n if (!fs.existsSync(filePath)) {\n return filePath;\n }\n counter++;\n }\n}\n\nexport function deleteFile(filePath: string) {\n if (fs.existsSync(filePath)) {\n fs.unlinkSync(filePath);\n }\n}\n\n/**\n * 임시파일 삭제\n */\nexport function deleteTempDiff() {\n deleteFile(tempDiffPath);\n}\n\n/**\n * 리뷰 결과 폴더 생성\n */\nexport function createReportDirectory() {\n if (!fs.existsSync(REPORT_DIR)) {\n fs.mkdirSync(REPORT_DIR, { recursive: true });\n }\n}\n\n/**\n * 현재 시간 문자열 생성\n */\nexport function getNowString() {\n const now = new Date();\n const YYYY = now.getFullYear();\n const MM = String(now.getMonth() + 1).padStart(2, '0');\n const DD = String(now.getDate()).padStart(2, '0');\n const HH = String(now.getHours()).padStart(2, '0');\n const mm = String(now.getMinutes()).padStart(2, '0');\n const ss = String(now.getSeconds()).padStart(2, '0');\n\n return `${YYYY}-${MM}-${DD}_${HH}-${mm}-${ss}`;\n}\n\nexport function getGitDiffFilter() {\n // 1. 리뷰 대상 파일 확장자 정의\n const includeExtensions = ['*.ts', '*.tsx', '*.js', '*.jsx'];\n\n // ignoreList 를 import 하여 재사용하여 작성한다.\n const excludePatterns = ignoreList.map((item) => `:(exclude)${item}`);\n\n // const excludePatterns = [':(exclude)*.lock', ':(exclude)dist/', ':(exclude)*.md'];\n\n // 2. 변경된 파일 목록 가져오기 (각 패턴을 따옴표로 감싸서 쉘 에러 방지)\n const quote = (pattern: string) => `\"${pattern}\"`;\n const includeParams = includeExtensions.map(quote).join(' ');\n const excludeParams = excludePatterns.map(quote).join(' ');\n\n return { includeParams, excludeParams };\n}\n\n/**\n * openReport를 OS별로 동작하도록 변경\n * 우선순위:\n * 1. Chrome 시도\n * - macOS: open -a \"Google Chrome\" \"<path>\"\n * - Ubuntu/Linux: google-chrome \"<path>\"\n * 2. 실패 시 기본 브라우저로 폴백\n * - macOS: open \"<path>\"\n * - Ubuntu/Linux: xdg-open \"<path>\"\n * 3. 둘 다 실패하면 에러 출력\n * 4. 미지원 플랫폼이면 플랫폼 경고 출력\n */\nexport function openReport(reportPath: string) {\n const resolvedPath = path.resolve(reportPath);\n const { platform } = process;\n\n const openWithChrome = () => {\n if (platform === 'darwin') {\n execSync(`open -a \"Google Chrome\" \"${resolvedPath}\"`, { stdio: 'ignore' });\n\n return true;\n }\n\n if (platform === 'linux') {\n execSync(`google-chrome \"${resolvedPath}\"`, { stdio: 'ignore' });\n\n return true;\n }\n\n return false;\n };\n\n const openWithDefaultBrowser = () => {\n if (platform === 'darwin') {\n execSync(`open \"${resolvedPath}\"`, { stdio: 'ignore' });\n\n return true;\n }\n\n if (platform === 'linux') {\n execSync(`xdg-open \"${resolvedPath}\"`, { stdio: 'ignore' });\n\n return true;\n }\n\n return false;\n };\n\n try {\n if (openWithChrome()) {\n console.log('🚀 Google Chrome에서 리포트를 열었습니다.');\n\n return;\n }\n } catch {\n // Chrome 실행 실패 시 기본 브라우저로 폴백\n }\n\n try {\n if (openWithDefaultBrowser()) {\n console.log('🚀 기본 브라우저에서 리포트를 열었습니다.');\n\n return;\n }\n } catch (e) {\n console.error('⚠️ 브라우저 열기 실패:', e);\n\n return;\n }\n\n console.error(`⚠️ 지원하지 않는 플랫폼입니다: ${platform}`);\n}\n\nexport function getDiffArgs() {\n const args = process.argv.slice(2);\n const commitIndex = args.indexOf('--commit');\n const { includeParams, excludeParams } = getGitDiffFilter();\n\n let diffArgs = '';\n\n if (commitIndex !== -1) {\n // 특정 커밋 (및 이전 n개) 리뷰\n const commitHash = args[commitIndex + 1];\n if (!commitHash) {\n console.error('❌ 커밋 해시가 제공되지 않았습니다.');\n process.exit(1);\n }\n\n // n값 확인 (optional)\n const nextArg = args[commitIndex + 2];\n let n = 0;\n if (nextArg && !nextArg.startsWith('--')) {\n n = parseInt(nextArg, 10);\n if (isNaN(n)) {\n n = 0;\n }\n }\n\n console.log(`ℹ️ 커밋 '${commitHash}' ${n > 0 ? ` 포함 총 ${n + 1}개의 커밋` : ''}을 리뷰합니다...`);\n diffArgs = `${commitHash}~${n + 1} ${commitHash}`;\n } else {\n // 기본 모드:\n // 1. Unstaged 변경사항 확인\n try {\n const check = execSync(`git diff --name-only -- ${includeParams} ${excludeParams}`).toString();\n if (!check.trim()) {\n console.log('ℹ️ Unstaged 변경사항이 없습니다. 마지막 커밋(HEAD)을 리뷰합니다...');\n diffArgs = 'HEAD~1 HEAD';\n }\n } catch {\n // git diff 실패시 무시\n }\n }\n\n return diffArgs;\n}\n\n// export const ServiceType = {\n// GEMINI: 'gemini',\n// CLAUDE: 'claude',\n// CODEX: 'codex'\n\n// }\n\n/**\n * AI 서비스 선택\n */\nexport function selectAIService() {\n const args = process.argv.slice(2);\n const serviceIndex = args.indexOf('--service');\n const service = args[serviceIndex + 1];\n if (!service) {\n console.error('❌ 서비스가 선택되지 않았습니다.');\n process.exit(1);\n }\n\n return service;\n}\n\n/**\n * 터미널에서 라디오 버튼 형태로 AI 서비스를 선택합니다.\n */\nexport async function showSelectionAIService(): Promise<AIServiceType> {\n let selectedIndex = 0;\n\n // Use readline to handle keypresses\n // 키 입력을 처리하기 위해 readline 인터페이스 사용\n const rl = readline.createInterface({\n input: process.stdin,\n output: process.stdout,\n terminal: true\n });\n\n let firstRender = true;\n\n // Hide cursor\n process.stdout.write('\\u001b[?25l');\n\n const render = () => {\n if (!firstRender) {\n // Move cursor back to the starting line of the selection UI\n // We print (1 question line + services.length lines)\n // 선택 UI의 시작 라인으로 커서 이동 (질문 1줄 + 서비스 목록 N줄)\n readline.moveCursor(process.stdout, 0, -(AIServices.length + 1));\n }\n firstRender = false;\n\n // Clear everything from cursor down to avoid ghosting/overlaps\n // 잔상이나 겹침을 방지하기 위해 커서 위치부터 아래쪽 모두 지움\n readline.clearScreenDown(process.stdout);\n\n process.stdout.write(\n '🤖 AI 서비스를 선택해주세요 (\\u001b[33m↑↓ 방향키\\u001b[0m 이동, \\u001b[33mEnter\\u001b[0m 선택):\\n'\n );\n AIServices.forEach((service, index) => {\n if (index === selectedIndex) {\n process.stdout.write(` \\u001b[36m>\\u001b[0m \\u001b[36m◉\\u001b[0m \\u001b[1m${service}\\u001b[0m\\n`);\n } else {\n process.stdout.write(` ◯ ${service}\\n`);\n }\n });\n };\n\n render();\n\n return new Promise((resolve) => {\n const onData = (data: Buffer) => {\n const key = data.toString();\n if (key === '\\u0003') {\n // Ctrl+C\n process.stdout.write('\\u001b[?25h'); // Show cursor\n process.exit(0);\n }\n if (key === '\\x1b[A') {\n // Up arrow\n selectedIndex = (selectedIndex - 1 + AIServices.length) % AIServices.length;\n render();\n } else if (key === '\\x1b[B') {\n // Down arrow\n selectedIndex = (selectedIndex + 1) % AIServices.length;\n render();\n } else if (key === '\\r' || key === '\\n') {\n // Enter\n process.stdin.removeListener('data', onData);\n process.stdin.setRawMode(false);\n process.stdin.pause();\n rl.close();\n\n // Show cursor\n process.stdout.write('\\u001b[?25h');\n\n console.log(`\\n✅ \\u001b[32m${AIServices[selectedIndex]}\\u001b[0m 서비스가 선택되었습니다.\\n`);\n const result = AIServices[selectedIndex];\n if (result) {\n resolve(result);\n }\n }\n };\n\n process.stdin.setRawMode(true);\n process.stdin.resume();\n process.stdin.on('data', onData);\n });\n}\n","import fs from 'fs';\n\nimport { codingConventionRulesPath, createTraceLogger, namingRulesPath, rulesPath } from '../../common/helper';\n\nconst args = process.argv.slice(2);\nconst trace = createTraceLogger('codex-commander', args);\n\n/**\n * @description\n * Codex CLI를 활용한 코드 리뷰 명령어를 생성합니다.\n *\n * 모델 기본값:\n * - --review: gpt-5 (정밀 리뷰)\n * - --flash: gpt-5-mini (빠른 리뷰)\n * - 미지정: gpt-5-mini\n */\nexport const createCodexCommand = (tempDiffPath: string, reviewFormPath: string) => {\n trace('createCodexCommand:start', `tempDiffPath=${tempDiffPath}, reviewFormPath=${reviewFormPath}`);\n let modelOption = '';\n\n if (args.includes('--review')) {\n modelOption = '--model gpt-5';\n trace('model:review', modelOption);\n } else if (args.includes('--flash')) {\n modelOption = '--model gpt-5-mini';\n trace('model:flash', modelOption);\n } else {\n const modelIndex = args.indexOf('--model');\n if (modelIndex !== -1 && args[modelIndex + 1]) {\n console.warn('⚠️ 지정한 모델이 없는 경우, 에러가 발생하니 주의하세요.');\n modelOption = `--model ${args[modelIndex + 1]}`;\n trace('model:custom', modelOption);\n } else {\n console.warn('⚠️ 모델이 지정되지 않았습니다. 기본 모델인 gpt-5-mini를 사용합니다.');\n modelOption = '--model gpt-5-mini';\n trace('model:default', modelOption);\n }\n }\n\n const rules = [rulesPath, namingRulesPath, codingConventionRulesPath]\n .filter((filePath) => fs.existsSync(filePath))\n .map((filePath) => `- ${filePath}`)\n .join('\\n');\n const rulesCount = rules ? rules.split('\\n').length : 0;\n trace('rules:loaded', `count=${rulesCount}`);\n\n const hasReviewForm = fs.existsSync(reviewFormPath);\n const reviewFormLine = hasReviewForm ? `- ${reviewFormPath}` : '';\n trace('reviewForm:status', reviewFormLine ? 'exists' : 'missing');\n\n const prompt = `아래 파일들을 참고해서 코드 리뷰를 진행해줘.\n규칙 파일: \n${rules || '- (없음)'}\n리뷰 양식 파일:\n${reviewFormLine || '- (없음)'}\n리뷰 대상 diff 파일:\n- ${tempDiffPath}\n\n반드시 리뷰 양식에 맞춰 작성해줘.`;\n\n const command = `codex exec ${modelOption} \"${prompt.replace(/\"/g, '\\\\\"')}\"`;\n trace('command:created');\n\n if (args.includes('--test')) {\n const safeCommand = command.replace(/\"/g, '\\\\\"');\n trace('test-mode:return-preview');\n\n return `echo \"[TEST MODE] Codex 명령어가 실행되지 않았습니다.\\n\\n생성될 명령어 미리보기:\\n${safeCommand}\"`;\n }\n\n trace('createCodexCommand:end');\n\n return command;\n};\n"]}
@@ -0,0 +1,62 @@
1
+ 'use strict';
2
+
3
+ var child_process = require('child_process');
4
+ var path = require('path');
5
+ var url = require('url');
6
+
7
+ var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
8
+ function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
9
+
10
+ var path__default = /*#__PURE__*/_interopDefault(path);
11
+
12
+ // src/pr-review/codex/installation-codex.ts
13
+ var __dirname$1 = path__default.default.dirname(url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('installation-codex.cjs', document.baseURI).href))));
14
+ path__default.default.resolve(__dirname$1, "../../src/common/rules/review-rules.md");
15
+ path__default.default.resolve(__dirname$1, "../../src/common/rules/naming-rule.md");
16
+ path__default.default.resolve(__dirname$1, "../../src/common/rules/coding-convention.md");
17
+ path__default.default.resolve(__dirname$1, "../../src/common/form/review-form.md");
18
+ path__default.default.resolve(__dirname$1, "../../src/common/form/review-form-one-by-one.md");
19
+ function isTestMode(args = process.argv.slice(2)) {
20
+ return args.includes("--test");
21
+ }
22
+ function createTraceLogger(scope, args = process.argv.slice(2)) {
23
+ const enabled = isTestMode(args);
24
+ return (step, detail) => {
25
+ if (!enabled) {
26
+ return;
27
+ }
28
+ console.log(`[TRACE][${scope}] ${step}${detail ? ` | ${detail}` : ""}`);
29
+ };
30
+ }
31
+
32
+ // src/pr-review/codex/installation-codex.ts
33
+ var trace = createTraceLogger("installation-codex");
34
+ function checkCodexCliInstalled() {
35
+ trace("checkCodexCliInstalled:start");
36
+ try {
37
+ trace("version-check:run", "codex --version");
38
+ child_process.execSync("codex --version", { stdio: "ignore" });
39
+ trace("version-check:ok");
40
+ } catch {
41
+ trace("version-check:failed", "install-start");
42
+ console.log("\u2139\uFE0F codex-cli\uAC00 \uC124\uCE58\uB418\uC5B4 \uC788\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4. \uC124\uCE58\uB97C \uC9C4\uD589\uD569\uB2C8\uB2E4... npm install -g @openai/codex");
43
+ try {
44
+ child_process.execSync("npm install -g @openai/codex", { stdio: "inherit" });
45
+ trace("install:ok", "exit(1) for login");
46
+ console.log("\u2705 codex-cli \uC124\uCE58\uAC00 \uC644\uB8CC\uB418\uC5C8\uC2B5\uB2C8\uB2E4.");
47
+ console.log("\u26A0\uFE0F codex-cli \uC0AC\uC6A9\uC744 \uC704\uD574 \uC778\uC99D\uC774 \uD544\uC694\uD569\uB2C8\uB2E4.");
48
+ console.log(' \uD130\uBBF8\uB110\uC5D0\uC11C "codex login" \uC744 \uC785\uB825\uD558\uC5EC \uB85C\uADF8\uC778\uC744 \uC644\uB8CC\uD55C \uD6C4, \uB2E4\uC2DC \uC2DC\uB3C4\uD574\uC8FC\uC138\uC694.');
49
+ process.exit(1);
50
+ } catch (installError) {
51
+ trace("install:failed");
52
+ console.error("\u274C codex-cli \uC124\uCE58 \uC911 \uC624\uB958\uAC00 \uBC1C\uC0DD\uD588\uC2B5\uB2C8\uB2E4. \uAD8C\uD55C \uBB38\uC81C\uC77C \uC218 \uC788\uC2B5\uB2C8\uB2E4 (sudo \uD544\uC694).");
53
+ console.error(installError);
54
+ process.exit(1);
55
+ }
56
+ }
57
+ trace("checkCodexCliInstalled:end");
58
+ }
59
+
60
+ exports.checkCodexCliInstalled = checkCodexCliInstalled;
61
+ //# sourceMappingURL=installation-codex.cjs.map
62
+ //# sourceMappingURL=installation-codex.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/common/helper.ts","../../../src/pr-review/codex/installation-codex.ts"],"names":["__dirname","path","fileURLToPath","execSync"],"mappings":";;;;;;;;;;;;AAQA,IAAMA,cAAYC,qBAAK,CAAA,OAAA,CAAQC,iBAAc,CAAA,wQAAe,CAAC,CAAA;AAKpCD,qBAAA,CAAK,OAAQ,CAAAD,WAAA,EAAW,wCAAwC;AAC1DC,qBAAA,CAAK,OAAQ,CAAAD,WAAA,EAAW,uCAAuC;AACrDC,qBAAA,CAAK,OAAQ,CAAAD,WAAA,EAAW,6CAA6C;AAChFC,qBAAA,CAAK,OAAQ,CAAAD,WAAA,EAAW,sCAAsC;AACtDC,qBAAA,CAAK,OAAQ,CAAAD,WAAA,EAAW,iDAAiD;AAkBxG,SAAS,WAAW,IAAiB,GAAA,OAAA,CAAQ,IAAK,CAAA,KAAA,CAAM,CAAC,CAAG,EAAA;AACjE,EAAO,OAAA,IAAA,CAAK,SAAS,QAAQ,CAAA;AAC/B;AAEO,SAAS,kBAAkB,KAAe,EAAA,IAAA,GAAiB,QAAQ,IAAK,CAAA,KAAA,CAAM,CAAC,CAAG,EAAA;AACvF,EAAM,MAAA,OAAA,GAAU,WAAW,IAAI,CAAA;AAE/B,EAAO,OAAA,CAAC,MAAc,MAAoB,KAAA;AACxC,IAAA,IAAI,CAAC,OAAS,EAAA;AACZ,MAAA;AAAA;AAGF,IAAQ,OAAA,CAAA,GAAA,CAAI,CAAW,QAAA,EAAA,KAAK,CAAK,EAAA,EAAA,IAAI,CAAG,EAAA,MAAA,GAAS,CAAM,GAAA,EAAA,MAAM,CAAK,CAAA,GAAA,EAAE,CAAE,CAAA,CAAA;AAAA,GACxE;AACF;;;AC7CA,IAAM,KAAA,GAAQ,kBAAkB,oBAAoB,CAAA;AAG7C,SAAS,sBAAyB,GAAA;AACvC,EAAA,KAAA,CAAM,8BAA8B,CAAA;AACpC,EAAI,IAAA;AACF,IAAA,KAAA,CAAM,qBAAqB,iBAAiB,CAAA;AAC5C,IAAAG,sBAAA,CAAS,iBAAmB,EAAA,EAAE,KAAO,EAAA,QAAA,EAAU,CAAA;AAC/C,IAAA,KAAA,CAAM,kBAAkB,CAAA;AAAA,GAClB,CAAA,MAAA;AACN,IAAA,KAAA,CAAM,wBAAwB,eAAe,CAAA;AAC7C,IAAA,OAAA,CAAQ,IAAI,gLAAuE,CAAA;AACnF,IAAI,IAAA;AACF,MAAAA,sBAAA,CAAS,8BAAgC,EAAA,EAAE,KAAO,EAAA,SAAA,EAAW,CAAA;AAC7D,MAAA,KAAA,CAAM,cAAc,mBAAmB,CAAA;AACvC,MAAA,OAAA,CAAQ,IAAI,iFAA0B,CAAA;AACtC,MAAA,OAAA,CAAQ,IAAI,2GAAgC,CAAA;AAC5C,MAAA,OAAA,CAAQ,IAAI,wLAAsD,CAAA;AAClE,MAAA,OAAA,CAAQ,KAAK,CAAC,CAAA;AAAA,aACP,YAAc,EAAA;AACrB,MAAA,KAAA,CAAM,gBAAgB,CAAA;AACtB,MAAA,OAAA,CAAQ,MAAM,oLAAuD,CAAA;AACrE,MAAA,OAAA,CAAQ,MAAM,YAAY,CAAA;AAC1B,MAAA,OAAA,CAAQ,KAAK,CAAC,CAAA;AAAA;AAChB;AAEF,EAAA,KAAA,CAAM,4BAA4B,CAAA;AACpC","file":"installation-codex.cjs","sourcesContent":["import { execSync } from 'child_process';\nimport fs from 'fs';\nimport path from 'path';\nimport readline from 'readline';\nimport { fileURLToPath } from 'url';\n\nimport { AIServiceType } from './types';\n\nconst __dirname = path.dirname(fileURLToPath(import.meta.url));\n\n// 설치된 위치에 맞게 규칙/양식 파일 경로를 계산 (dist에서 src로 이동 등 고려)\n// dist/common/helper.js 가 실행되므로 __dirname은 .../dist/common 입니다.\n// 따라서 ../../src/common/rules 로 이동해야 원본 소스의 규칙 파일을 찾을 수 있습니다.\nexport const rulesPath = path.resolve(__dirname, '../../src/common/rules/review-rules.md');\nexport const namingRulesPath = path.resolve(__dirname, '../../src/common/rules/naming-rule.md');\nexport const codingConventionRulesPath = path.resolve(__dirname, '../../src/common/rules/coding-convention.md');\nexport const reviewFormPath = path.resolve(__dirname, '../../src/common/form/review-form.md');\nexport const reviewFormOneByOnePath = path.resolve(__dirname, '../../src/common/form/review-form-one-by-one.md');\nexport const REPORT_DIR = '.review-report';\nexport const tempDiffPath = 'temp_diff.txt';\nexport const AIServices: AIServiceType[] = ['gemini', 'claude', 'codex'];\nexport const ignoreList = [\n 'package.json',\n '*.yml',\n '*.md',\n '*.lock',\n 'dist/',\n 'node_modules/',\n 'assets/',\n 'public/',\n '*.json',\n '*.yaml',\n '.review-report/' // 생성되는 리포트 폴더도 제외\n];\n\nexport function isTestMode(args: string[] = process.argv.slice(2)) {\n return args.includes('--test');\n}\n\nexport function createTraceLogger(scope: string, args: string[] = process.argv.slice(2)) {\n const enabled = isTestMode(args);\n\n return (step: string, detail?: string) => {\n if (!enabled) {\n return;\n }\n\n console.log(`[TRACE][${scope}] ${step}${detail ? ` | ${detail}` : ''}`);\n };\n}\n\nexport function getNextFilePath(dir: string, baseName: string, extension: string) {\n let counter = 1;\n // eslint-disable-next-line no-constant-condition\n while (true) {\n const filePath = path.join(dir, `${baseName}-${counter}${extension}`);\n if (!fs.existsSync(filePath)) {\n return filePath;\n }\n counter++;\n }\n}\n\nexport function deleteFile(filePath: string) {\n if (fs.existsSync(filePath)) {\n fs.unlinkSync(filePath);\n }\n}\n\n/**\n * 임시파일 삭제\n */\nexport function deleteTempDiff() {\n deleteFile(tempDiffPath);\n}\n\n/**\n * 리뷰 결과 폴더 생성\n */\nexport function createReportDirectory() {\n if (!fs.existsSync(REPORT_DIR)) {\n fs.mkdirSync(REPORT_DIR, { recursive: true });\n }\n}\n\n/**\n * 현재 시간 문자열 생성\n */\nexport function getNowString() {\n const now = new Date();\n const YYYY = now.getFullYear();\n const MM = String(now.getMonth() + 1).padStart(2, '0');\n const DD = String(now.getDate()).padStart(2, '0');\n const HH = String(now.getHours()).padStart(2, '0');\n const mm = String(now.getMinutes()).padStart(2, '0');\n const ss = String(now.getSeconds()).padStart(2, '0');\n\n return `${YYYY}-${MM}-${DD}_${HH}-${mm}-${ss}`;\n}\n\nexport function getGitDiffFilter() {\n // 1. 리뷰 대상 파일 확장자 정의\n const includeExtensions = ['*.ts', '*.tsx', '*.js', '*.jsx'];\n\n // ignoreList 를 import 하여 재사용하여 작성한다.\n const excludePatterns = ignoreList.map((item) => `:(exclude)${item}`);\n\n // const excludePatterns = [':(exclude)*.lock', ':(exclude)dist/', ':(exclude)*.md'];\n\n // 2. 변경된 파일 목록 가져오기 (각 패턴을 따옴표로 감싸서 쉘 에러 방지)\n const quote = (pattern: string) => `\"${pattern}\"`;\n const includeParams = includeExtensions.map(quote).join(' ');\n const excludeParams = excludePatterns.map(quote).join(' ');\n\n return { includeParams, excludeParams };\n}\n\n/**\n * openReport를 OS별로 동작하도록 변경\n * 우선순위:\n * 1. Chrome 시도\n * - macOS: open -a \"Google Chrome\" \"<path>\"\n * - Ubuntu/Linux: google-chrome \"<path>\"\n * 2. 실패 시 기본 브라우저로 폴백\n * - macOS: open \"<path>\"\n * - Ubuntu/Linux: xdg-open \"<path>\"\n * 3. 둘 다 실패하면 에러 출력\n * 4. 미지원 플랫폼이면 플랫폼 경고 출력\n */\nexport function openReport(reportPath: string) {\n const resolvedPath = path.resolve(reportPath);\n const { platform } = process;\n\n const openWithChrome = () => {\n if (platform === 'darwin') {\n execSync(`open -a \"Google Chrome\" \"${resolvedPath}\"`, { stdio: 'ignore' });\n\n return true;\n }\n\n if (platform === 'linux') {\n execSync(`google-chrome \"${resolvedPath}\"`, { stdio: 'ignore' });\n\n return true;\n }\n\n return false;\n };\n\n const openWithDefaultBrowser = () => {\n if (platform === 'darwin') {\n execSync(`open \"${resolvedPath}\"`, { stdio: 'ignore' });\n\n return true;\n }\n\n if (platform === 'linux') {\n execSync(`xdg-open \"${resolvedPath}\"`, { stdio: 'ignore' });\n\n return true;\n }\n\n return false;\n };\n\n try {\n if (openWithChrome()) {\n console.log('🚀 Google Chrome에서 리포트를 열었습니다.');\n\n return;\n }\n } catch {\n // Chrome 실행 실패 시 기본 브라우저로 폴백\n }\n\n try {\n if (openWithDefaultBrowser()) {\n console.log('🚀 기본 브라우저에서 리포트를 열었습니다.');\n\n return;\n }\n } catch (e) {\n console.error('⚠️ 브라우저 열기 실패:', e);\n\n return;\n }\n\n console.error(`⚠️ 지원하지 않는 플랫폼입니다: ${platform}`);\n}\n\nexport function getDiffArgs() {\n const args = process.argv.slice(2);\n const commitIndex = args.indexOf('--commit');\n const { includeParams, excludeParams } = getGitDiffFilter();\n\n let diffArgs = '';\n\n if (commitIndex !== -1) {\n // 특정 커밋 (및 이전 n개) 리뷰\n const commitHash = args[commitIndex + 1];\n if (!commitHash) {\n console.error('❌ 커밋 해시가 제공되지 않았습니다.');\n process.exit(1);\n }\n\n // n값 확인 (optional)\n const nextArg = args[commitIndex + 2];\n let n = 0;\n if (nextArg && !nextArg.startsWith('--')) {\n n = parseInt(nextArg, 10);\n if (isNaN(n)) {\n n = 0;\n }\n }\n\n console.log(`ℹ️ 커밋 '${commitHash}' ${n > 0 ? ` 포함 총 ${n + 1}개의 커밋` : ''}을 리뷰합니다...`);\n diffArgs = `${commitHash}~${n + 1} ${commitHash}`;\n } else {\n // 기본 모드:\n // 1. Unstaged 변경사항 확인\n try {\n const check = execSync(`git diff --name-only -- ${includeParams} ${excludeParams}`).toString();\n if (!check.trim()) {\n console.log('ℹ️ Unstaged 변경사항이 없습니다. 마지막 커밋(HEAD)을 리뷰합니다...');\n diffArgs = 'HEAD~1 HEAD';\n }\n } catch {\n // git diff 실패시 무시\n }\n }\n\n return diffArgs;\n}\n\n// export const ServiceType = {\n// GEMINI: 'gemini',\n// CLAUDE: 'claude',\n// CODEX: 'codex'\n\n// }\n\n/**\n * AI 서비스 선택\n */\nexport function selectAIService() {\n const args = process.argv.slice(2);\n const serviceIndex = args.indexOf('--service');\n const service = args[serviceIndex + 1];\n if (!service) {\n console.error('❌ 서비스가 선택되지 않았습니다.');\n process.exit(1);\n }\n\n return service;\n}\n\n/**\n * 터미널에서 라디오 버튼 형태로 AI 서비스를 선택합니다.\n */\nexport async function showSelectionAIService(): Promise<AIServiceType> {\n let selectedIndex = 0;\n\n // Use readline to handle keypresses\n // 키 입력을 처리하기 위해 readline 인터페이스 사용\n const rl = readline.createInterface({\n input: process.stdin,\n output: process.stdout,\n terminal: true\n });\n\n let firstRender = true;\n\n // Hide cursor\n process.stdout.write('\\u001b[?25l');\n\n const render = () => {\n if (!firstRender) {\n // Move cursor back to the starting line of the selection UI\n // We print (1 question line + services.length lines)\n // 선택 UI의 시작 라인으로 커서 이동 (질문 1줄 + 서비스 목록 N줄)\n readline.moveCursor(process.stdout, 0, -(AIServices.length + 1));\n }\n firstRender = false;\n\n // Clear everything from cursor down to avoid ghosting/overlaps\n // 잔상이나 겹침을 방지하기 위해 커서 위치부터 아래쪽 모두 지움\n readline.clearScreenDown(process.stdout);\n\n process.stdout.write(\n '🤖 AI 서비스를 선택해주세요 (\\u001b[33m↑↓ 방향키\\u001b[0m 이동, \\u001b[33mEnter\\u001b[0m 선택):\\n'\n );\n AIServices.forEach((service, index) => {\n if (index === selectedIndex) {\n process.stdout.write(` \\u001b[36m>\\u001b[0m \\u001b[36m◉\\u001b[0m \\u001b[1m${service}\\u001b[0m\\n`);\n } else {\n process.stdout.write(` ◯ ${service}\\n`);\n }\n });\n };\n\n render();\n\n return new Promise((resolve) => {\n const onData = (data: Buffer) => {\n const key = data.toString();\n if (key === '\\u0003') {\n // Ctrl+C\n process.stdout.write('\\u001b[?25h'); // Show cursor\n process.exit(0);\n }\n if (key === '\\x1b[A') {\n // Up arrow\n selectedIndex = (selectedIndex - 1 + AIServices.length) % AIServices.length;\n render();\n } else if (key === '\\x1b[B') {\n // Down arrow\n selectedIndex = (selectedIndex + 1) % AIServices.length;\n render();\n } else if (key === '\\r' || key === '\\n') {\n // Enter\n process.stdin.removeListener('data', onData);\n process.stdin.setRawMode(false);\n process.stdin.pause();\n rl.close();\n\n // Show cursor\n process.stdout.write('\\u001b[?25h');\n\n console.log(`\\n✅ \\u001b[32m${AIServices[selectedIndex]}\\u001b[0m 서비스가 선택되었습니다.\\n`);\n const result = AIServices[selectedIndex];\n if (result) {\n resolve(result);\n }\n }\n };\n\n process.stdin.setRawMode(true);\n process.stdin.resume();\n process.stdin.on('data', onData);\n });\n}\n","import { execSync } from 'child_process';\n\nimport { createTraceLogger } from '../../common/helper';\n\nconst trace = createTraceLogger('installation-codex');\n\n// codex-cli 설치 확인 및 설치\nexport function checkCodexCliInstalled() {\n trace('checkCodexCliInstalled:start');\n try {\n trace('version-check:run', 'codex --version');\n execSync('codex --version', { stdio: 'ignore' });\n trace('version-check:ok');\n } catch {\n trace('version-check:failed', 'install-start');\n console.log('ℹ️ codex-cli가 설치되어 있지 않습니다. 설치를 진행합니다... npm install -g @openai/codex');\n try {\n execSync('npm install -g @openai/codex', { stdio: 'inherit' });\n trace('install:ok', 'exit(1) for login');\n console.log('✅ codex-cli 설치가 완료되었습니다.');\n console.log('⚠️ codex-cli 사용을 위해 인증이 필요합니다.');\n console.log(' 터미널에서 \"codex login\" 을 입력하여 로그인을 완료한 후, 다시 시도해주세요.');\n process.exit(1);\n } catch (installError) {\n trace('install:failed');\n console.error('❌ codex-cli 설치 중 오류가 발생했습니다. 권한 문제일 수 있습니다 (sudo 필요).');\n console.error(installError);\n process.exit(1);\n }\n }\n trace('checkCodexCliInstalled:end');\n}\n"]}
@@ -0,0 +1,3 @@
1
+ declare function checkCodexCliInstalled(): void;
2
+
3
+ export { checkCodexCliInstalled };
@@ -0,0 +1,3 @@
1
+ declare function checkCodexCliInstalled(): void;
2
+
3
+ export { checkCodexCliInstalled };
@@ -0,0 +1,55 @@
1
+ import { execSync } from 'child_process';
2
+ import path from 'path';
3
+ import { fileURLToPath } from 'url';
4
+
5
+ // src/pr-review/codex/installation-codex.ts
6
+ var __dirname = path.dirname(fileURLToPath(import.meta.url));
7
+ path.resolve(__dirname, "../../src/common/rules/review-rules.md");
8
+ path.resolve(__dirname, "../../src/common/rules/naming-rule.md");
9
+ path.resolve(__dirname, "../../src/common/rules/coding-convention.md");
10
+ path.resolve(__dirname, "../../src/common/form/review-form.md");
11
+ path.resolve(__dirname, "../../src/common/form/review-form-one-by-one.md");
12
+ function isTestMode(args = process.argv.slice(2)) {
13
+ return args.includes("--test");
14
+ }
15
+ function createTraceLogger(scope, args = process.argv.slice(2)) {
16
+ const enabled = isTestMode(args);
17
+ return (step, detail) => {
18
+ if (!enabled) {
19
+ return;
20
+ }
21
+ console.log(`[TRACE][${scope}] ${step}${detail ? ` | ${detail}` : ""}`);
22
+ };
23
+ }
24
+
25
+ // src/pr-review/codex/installation-codex.ts
26
+ var trace = createTraceLogger("installation-codex");
27
+ function checkCodexCliInstalled() {
28
+ trace("checkCodexCliInstalled:start");
29
+ try {
30
+ trace("version-check:run", "codex --version");
31
+ execSync("codex --version", { stdio: "ignore" });
32
+ trace("version-check:ok");
33
+ } catch {
34
+ trace("version-check:failed", "install-start");
35
+ console.log("\u2139\uFE0F codex-cli\uAC00 \uC124\uCE58\uB418\uC5B4 \uC788\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4. \uC124\uCE58\uB97C \uC9C4\uD589\uD569\uB2C8\uB2E4... npm install -g @openai/codex");
36
+ try {
37
+ execSync("npm install -g @openai/codex", { stdio: "inherit" });
38
+ trace("install:ok", "exit(1) for login");
39
+ console.log("\u2705 codex-cli \uC124\uCE58\uAC00 \uC644\uB8CC\uB418\uC5C8\uC2B5\uB2C8\uB2E4.");
40
+ console.log("\u26A0\uFE0F codex-cli \uC0AC\uC6A9\uC744 \uC704\uD574 \uC778\uC99D\uC774 \uD544\uC694\uD569\uB2C8\uB2E4.");
41
+ console.log(' \uD130\uBBF8\uB110\uC5D0\uC11C "codex login" \uC744 \uC785\uB825\uD558\uC5EC \uB85C\uADF8\uC778\uC744 \uC644\uB8CC\uD55C \uD6C4, \uB2E4\uC2DC \uC2DC\uB3C4\uD574\uC8FC\uC138\uC694.');
42
+ process.exit(1);
43
+ } catch (installError) {
44
+ trace("install:failed");
45
+ console.error("\u274C codex-cli \uC124\uCE58 \uC911 \uC624\uB958\uAC00 \uBC1C\uC0DD\uD588\uC2B5\uB2C8\uB2E4. \uAD8C\uD55C \uBB38\uC81C\uC77C \uC218 \uC788\uC2B5\uB2C8\uB2E4 (sudo \uD544\uC694).");
46
+ console.error(installError);
47
+ process.exit(1);
48
+ }
49
+ }
50
+ trace("checkCodexCliInstalled:end");
51
+ }
52
+
53
+ export { checkCodexCliInstalled };
54
+ //# sourceMappingURL=installation-codex.js.map
55
+ //# sourceMappingURL=installation-codex.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/common/helper.ts","../../../src/pr-review/codex/installation-codex.ts"],"names":[],"mappings":";;;;;AAQA,IAAM,YAAY,IAAK,CAAA,OAAA,CAAQ,aAAc,CAAA,MAAA,CAAA,IAAA,CAAY,GAAG,CAAC,CAAA;AAKpC,IAAA,CAAK,OAAQ,CAAA,SAAA,EAAW,wCAAwC;AAC1D,IAAA,CAAK,OAAQ,CAAA,SAAA,EAAW,uCAAuC;AACrD,IAAA,CAAK,OAAQ,CAAA,SAAA,EAAW,6CAA6C;AAChF,IAAA,CAAK,OAAQ,CAAA,SAAA,EAAW,sCAAsC;AACtD,IAAA,CAAK,OAAQ,CAAA,SAAA,EAAW,iDAAiD;AAkBxG,SAAS,WAAW,IAAiB,GAAA,OAAA,CAAQ,IAAK,CAAA,KAAA,CAAM,CAAC,CAAG,EAAA;AACjE,EAAO,OAAA,IAAA,CAAK,SAAS,QAAQ,CAAA;AAC/B;AAEO,SAAS,kBAAkB,KAAe,EAAA,IAAA,GAAiB,QAAQ,IAAK,CAAA,KAAA,CAAM,CAAC,CAAG,EAAA;AACvF,EAAM,MAAA,OAAA,GAAU,WAAW,IAAI,CAAA;AAE/B,EAAO,OAAA,CAAC,MAAc,MAAoB,KAAA;AACxC,IAAA,IAAI,CAAC,OAAS,EAAA;AACZ,MAAA;AAAA;AAGF,IAAQ,OAAA,CAAA,GAAA,CAAI,CAAW,QAAA,EAAA,KAAK,CAAK,EAAA,EAAA,IAAI,CAAG,EAAA,MAAA,GAAS,CAAM,GAAA,EAAA,MAAM,CAAK,CAAA,GAAA,EAAE,CAAE,CAAA,CAAA;AAAA,GACxE;AACF;;;AC7CA,IAAM,KAAA,GAAQ,kBAAkB,oBAAoB,CAAA;AAG7C,SAAS,sBAAyB,GAAA;AACvC,EAAA,KAAA,CAAM,8BAA8B,CAAA;AACpC,EAAI,IAAA;AACF,IAAA,KAAA,CAAM,qBAAqB,iBAAiB,CAAA;AAC5C,IAAA,QAAA,CAAS,iBAAmB,EAAA,EAAE,KAAO,EAAA,QAAA,EAAU,CAAA;AAC/C,IAAA,KAAA,CAAM,kBAAkB,CAAA;AAAA,GAClB,CAAA,MAAA;AACN,IAAA,KAAA,CAAM,wBAAwB,eAAe,CAAA;AAC7C,IAAA,OAAA,CAAQ,IAAI,gLAAuE,CAAA;AACnF,IAAI,IAAA;AACF,MAAA,QAAA,CAAS,8BAAgC,EAAA,EAAE,KAAO,EAAA,SAAA,EAAW,CAAA;AAC7D,MAAA,KAAA,CAAM,cAAc,mBAAmB,CAAA;AACvC,MAAA,OAAA,CAAQ,IAAI,iFAA0B,CAAA;AACtC,MAAA,OAAA,CAAQ,IAAI,2GAAgC,CAAA;AAC5C,MAAA,OAAA,CAAQ,IAAI,wLAAsD,CAAA;AAClE,MAAA,OAAA,CAAQ,KAAK,CAAC,CAAA;AAAA,aACP,YAAc,EAAA;AACrB,MAAA,KAAA,CAAM,gBAAgB,CAAA;AACtB,MAAA,OAAA,CAAQ,MAAM,oLAAuD,CAAA;AACrE,MAAA,OAAA,CAAQ,MAAM,YAAY,CAAA;AAC1B,MAAA,OAAA,CAAQ,KAAK,CAAC,CAAA;AAAA;AAChB;AAEF,EAAA,KAAA,CAAM,4BAA4B,CAAA;AACpC","file":"installation-codex.js","sourcesContent":["import { execSync } from 'child_process';\nimport fs from 'fs';\nimport path from 'path';\nimport readline from 'readline';\nimport { fileURLToPath } from 'url';\n\nimport { AIServiceType } from './types';\n\nconst __dirname = path.dirname(fileURLToPath(import.meta.url));\n\n// 설치된 위치에 맞게 규칙/양식 파일 경로를 계산 (dist에서 src로 이동 등 고려)\n// dist/common/helper.js 가 실행되므로 __dirname은 .../dist/common 입니다.\n// 따라서 ../../src/common/rules 로 이동해야 원본 소스의 규칙 파일을 찾을 수 있습니다.\nexport const rulesPath = path.resolve(__dirname, '../../src/common/rules/review-rules.md');\nexport const namingRulesPath = path.resolve(__dirname, '../../src/common/rules/naming-rule.md');\nexport const codingConventionRulesPath = path.resolve(__dirname, '../../src/common/rules/coding-convention.md');\nexport const reviewFormPath = path.resolve(__dirname, '../../src/common/form/review-form.md');\nexport const reviewFormOneByOnePath = path.resolve(__dirname, '../../src/common/form/review-form-one-by-one.md');\nexport const REPORT_DIR = '.review-report';\nexport const tempDiffPath = 'temp_diff.txt';\nexport const AIServices: AIServiceType[] = ['gemini', 'claude', 'codex'];\nexport const ignoreList = [\n 'package.json',\n '*.yml',\n '*.md',\n '*.lock',\n 'dist/',\n 'node_modules/',\n 'assets/',\n 'public/',\n '*.json',\n '*.yaml',\n '.review-report/' // 생성되는 리포트 폴더도 제외\n];\n\nexport function isTestMode(args: string[] = process.argv.slice(2)) {\n return args.includes('--test');\n}\n\nexport function createTraceLogger(scope: string, args: string[] = process.argv.slice(2)) {\n const enabled = isTestMode(args);\n\n return (step: string, detail?: string) => {\n if (!enabled) {\n return;\n }\n\n console.log(`[TRACE][${scope}] ${step}${detail ? ` | ${detail}` : ''}`);\n };\n}\n\nexport function getNextFilePath(dir: string, baseName: string, extension: string) {\n let counter = 1;\n // eslint-disable-next-line no-constant-condition\n while (true) {\n const filePath = path.join(dir, `${baseName}-${counter}${extension}`);\n if (!fs.existsSync(filePath)) {\n return filePath;\n }\n counter++;\n }\n}\n\nexport function deleteFile(filePath: string) {\n if (fs.existsSync(filePath)) {\n fs.unlinkSync(filePath);\n }\n}\n\n/**\n * 임시파일 삭제\n */\nexport function deleteTempDiff() {\n deleteFile(tempDiffPath);\n}\n\n/**\n * 리뷰 결과 폴더 생성\n */\nexport function createReportDirectory() {\n if (!fs.existsSync(REPORT_DIR)) {\n fs.mkdirSync(REPORT_DIR, { recursive: true });\n }\n}\n\n/**\n * 현재 시간 문자열 생성\n */\nexport function getNowString() {\n const now = new Date();\n const YYYY = now.getFullYear();\n const MM = String(now.getMonth() + 1).padStart(2, '0');\n const DD = String(now.getDate()).padStart(2, '0');\n const HH = String(now.getHours()).padStart(2, '0');\n const mm = String(now.getMinutes()).padStart(2, '0');\n const ss = String(now.getSeconds()).padStart(2, '0');\n\n return `${YYYY}-${MM}-${DD}_${HH}-${mm}-${ss}`;\n}\n\nexport function getGitDiffFilter() {\n // 1. 리뷰 대상 파일 확장자 정의\n const includeExtensions = ['*.ts', '*.tsx', '*.js', '*.jsx'];\n\n // ignoreList 를 import 하여 재사용하여 작성한다.\n const excludePatterns = ignoreList.map((item) => `:(exclude)${item}`);\n\n // const excludePatterns = [':(exclude)*.lock', ':(exclude)dist/', ':(exclude)*.md'];\n\n // 2. 변경된 파일 목록 가져오기 (각 패턴을 따옴표로 감싸서 쉘 에러 방지)\n const quote = (pattern: string) => `\"${pattern}\"`;\n const includeParams = includeExtensions.map(quote).join(' ');\n const excludeParams = excludePatterns.map(quote).join(' ');\n\n return { includeParams, excludeParams };\n}\n\n/**\n * openReport를 OS별로 동작하도록 변경\n * 우선순위:\n * 1. Chrome 시도\n * - macOS: open -a \"Google Chrome\" \"<path>\"\n * - Ubuntu/Linux: google-chrome \"<path>\"\n * 2. 실패 시 기본 브라우저로 폴백\n * - macOS: open \"<path>\"\n * - Ubuntu/Linux: xdg-open \"<path>\"\n * 3. 둘 다 실패하면 에러 출력\n * 4. 미지원 플랫폼이면 플랫폼 경고 출력\n */\nexport function openReport(reportPath: string) {\n const resolvedPath = path.resolve(reportPath);\n const { platform } = process;\n\n const openWithChrome = () => {\n if (platform === 'darwin') {\n execSync(`open -a \"Google Chrome\" \"${resolvedPath}\"`, { stdio: 'ignore' });\n\n return true;\n }\n\n if (platform === 'linux') {\n execSync(`google-chrome \"${resolvedPath}\"`, { stdio: 'ignore' });\n\n return true;\n }\n\n return false;\n };\n\n const openWithDefaultBrowser = () => {\n if (platform === 'darwin') {\n execSync(`open \"${resolvedPath}\"`, { stdio: 'ignore' });\n\n return true;\n }\n\n if (platform === 'linux') {\n execSync(`xdg-open \"${resolvedPath}\"`, { stdio: 'ignore' });\n\n return true;\n }\n\n return false;\n };\n\n try {\n if (openWithChrome()) {\n console.log('🚀 Google Chrome에서 리포트를 열었습니다.');\n\n return;\n }\n } catch {\n // Chrome 실행 실패 시 기본 브라우저로 폴백\n }\n\n try {\n if (openWithDefaultBrowser()) {\n console.log('🚀 기본 브라우저에서 리포트를 열었습니다.');\n\n return;\n }\n } catch (e) {\n console.error('⚠️ 브라우저 열기 실패:', e);\n\n return;\n }\n\n console.error(`⚠️ 지원하지 않는 플랫폼입니다: ${platform}`);\n}\n\nexport function getDiffArgs() {\n const args = process.argv.slice(2);\n const commitIndex = args.indexOf('--commit');\n const { includeParams, excludeParams } = getGitDiffFilter();\n\n let diffArgs = '';\n\n if (commitIndex !== -1) {\n // 특정 커밋 (및 이전 n개) 리뷰\n const commitHash = args[commitIndex + 1];\n if (!commitHash) {\n console.error('❌ 커밋 해시가 제공되지 않았습니다.');\n process.exit(1);\n }\n\n // n값 확인 (optional)\n const nextArg = args[commitIndex + 2];\n let n = 0;\n if (nextArg && !nextArg.startsWith('--')) {\n n = parseInt(nextArg, 10);\n if (isNaN(n)) {\n n = 0;\n }\n }\n\n console.log(`ℹ️ 커밋 '${commitHash}' ${n > 0 ? ` 포함 총 ${n + 1}개의 커밋` : ''}을 리뷰합니다...`);\n diffArgs = `${commitHash}~${n + 1} ${commitHash}`;\n } else {\n // 기본 모드:\n // 1. Unstaged 변경사항 확인\n try {\n const check = execSync(`git diff --name-only -- ${includeParams} ${excludeParams}`).toString();\n if (!check.trim()) {\n console.log('ℹ️ Unstaged 변경사항이 없습니다. 마지막 커밋(HEAD)을 리뷰합니다...');\n diffArgs = 'HEAD~1 HEAD';\n }\n } catch {\n // git diff 실패시 무시\n }\n }\n\n return diffArgs;\n}\n\n// export const ServiceType = {\n// GEMINI: 'gemini',\n// CLAUDE: 'claude',\n// CODEX: 'codex'\n\n// }\n\n/**\n * AI 서비스 선택\n */\nexport function selectAIService() {\n const args = process.argv.slice(2);\n const serviceIndex = args.indexOf('--service');\n const service = args[serviceIndex + 1];\n if (!service) {\n console.error('❌ 서비스가 선택되지 않았습니다.');\n process.exit(1);\n }\n\n return service;\n}\n\n/**\n * 터미널에서 라디오 버튼 형태로 AI 서비스를 선택합니다.\n */\nexport async function showSelectionAIService(): Promise<AIServiceType> {\n let selectedIndex = 0;\n\n // Use readline to handle keypresses\n // 키 입력을 처리하기 위해 readline 인터페이스 사용\n const rl = readline.createInterface({\n input: process.stdin,\n output: process.stdout,\n terminal: true\n });\n\n let firstRender = true;\n\n // Hide cursor\n process.stdout.write('\\u001b[?25l');\n\n const render = () => {\n if (!firstRender) {\n // Move cursor back to the starting line of the selection UI\n // We print (1 question line + services.length lines)\n // 선택 UI의 시작 라인으로 커서 이동 (질문 1줄 + 서비스 목록 N줄)\n readline.moveCursor(process.stdout, 0, -(AIServices.length + 1));\n }\n firstRender = false;\n\n // Clear everything from cursor down to avoid ghosting/overlaps\n // 잔상이나 겹침을 방지하기 위해 커서 위치부터 아래쪽 모두 지움\n readline.clearScreenDown(process.stdout);\n\n process.stdout.write(\n '🤖 AI 서비스를 선택해주세요 (\\u001b[33m↑↓ 방향키\\u001b[0m 이동, \\u001b[33mEnter\\u001b[0m 선택):\\n'\n );\n AIServices.forEach((service, index) => {\n if (index === selectedIndex) {\n process.stdout.write(` \\u001b[36m>\\u001b[0m \\u001b[36m◉\\u001b[0m \\u001b[1m${service}\\u001b[0m\\n`);\n } else {\n process.stdout.write(` ◯ ${service}\\n`);\n }\n });\n };\n\n render();\n\n return new Promise((resolve) => {\n const onData = (data: Buffer) => {\n const key = data.toString();\n if (key === '\\u0003') {\n // Ctrl+C\n process.stdout.write('\\u001b[?25h'); // Show cursor\n process.exit(0);\n }\n if (key === '\\x1b[A') {\n // Up arrow\n selectedIndex = (selectedIndex - 1 + AIServices.length) % AIServices.length;\n render();\n } else if (key === '\\x1b[B') {\n // Down arrow\n selectedIndex = (selectedIndex + 1) % AIServices.length;\n render();\n } else if (key === '\\r' || key === '\\n') {\n // Enter\n process.stdin.removeListener('data', onData);\n process.stdin.setRawMode(false);\n process.stdin.pause();\n rl.close();\n\n // Show cursor\n process.stdout.write('\\u001b[?25h');\n\n console.log(`\\n✅ \\u001b[32m${AIServices[selectedIndex]}\\u001b[0m 서비스가 선택되었습니다.\\n`);\n const result = AIServices[selectedIndex];\n if (result) {\n resolve(result);\n }\n }\n };\n\n process.stdin.setRawMode(true);\n process.stdin.resume();\n process.stdin.on('data', onData);\n });\n}\n","import { execSync } from 'child_process';\n\nimport { createTraceLogger } from '../../common/helper';\n\nconst trace = createTraceLogger('installation-codex');\n\n// codex-cli 설치 확인 및 설치\nexport function checkCodexCliInstalled() {\n trace('checkCodexCliInstalled:start');\n try {\n trace('version-check:run', 'codex --version');\n execSync('codex --version', { stdio: 'ignore' });\n trace('version-check:ok');\n } catch {\n trace('version-check:failed', 'install-start');\n console.log('ℹ️ codex-cli가 설치되어 있지 않습니다. 설치를 진행합니다... npm install -g @openai/codex');\n try {\n execSync('npm install -g @openai/codex', { stdio: 'inherit' });\n trace('install:ok', 'exit(1) for login');\n console.log('✅ codex-cli 설치가 완료되었습니다.');\n console.log('⚠️ codex-cli 사용을 위해 인증이 필요합니다.');\n console.log(' 터미널에서 \"codex login\" 을 입력하여 로그인을 완료한 후, 다시 시도해주세요.');\n process.exit(1);\n } catch (installError) {\n trace('install:failed');\n console.error('❌ codex-cli 설치 중 오류가 발생했습니다. 권한 문제일 수 있습니다 (sudo 필요).');\n console.error(installError);\n process.exit(1);\n }\n }\n trace('checkCodexCliInstalled:end');\n}\n"]}
@@ -12,28 +12,46 @@ var path__default = /*#__PURE__*/_interopDefault(path);
12
12
 
13
13
  // src/pr-review/gemini/gemini-commander.ts
14
14
  var __dirname$1 = path__default.default.dirname(url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('gemini-commander.cjs', document.baseURI).href))));
15
- var rulesPath = path__default.default.resolve(__dirname$1, "../../src/pr-review/rules/review-rules.md");
16
- var namingRulesPath = path__default.default.resolve(__dirname$1, "../../src/pr-review/rules/naming-rule.md");
17
- var codingConventionRulesPath = path__default.default.resolve(__dirname$1, "../../src/pr-review/rules/coding-convention.md");
18
- path__default.default.resolve(__dirname$1, "../../src/pr-review/form/review-form.md");
19
- path__default.default.resolve(__dirname$1, "../../src/pr-review/form/review-form-one-by-one.md");
15
+ var rulesPath = path__default.default.resolve(__dirname$1, "../../src/common/rules/review-rules.md");
16
+ var namingRulesPath = path__default.default.resolve(__dirname$1, "../../src/common/rules/naming-rule.md");
17
+ var codingConventionRulesPath = path__default.default.resolve(__dirname$1, "../../src/common/rules/coding-convention.md");
18
+ path__default.default.resolve(__dirname$1, "../../src/common/form/review-form.md");
19
+ path__default.default.resolve(__dirname$1, "../../src/common/form/review-form-one-by-one.md");
20
+ function isTestMode(args2 = process.argv.slice(2)) {
21
+ return args2.includes("--test");
22
+ }
23
+ function createTraceLogger(scope, args2 = process.argv.slice(2)) {
24
+ const enabled = isTestMode(args2);
25
+ return (step, detail) => {
26
+ if (!enabled) {
27
+ return;
28
+ }
29
+ console.log(`[TRACE][${scope}] ${step}${detail ? ` | ${detail}` : ""}`);
30
+ };
31
+ }
20
32
 
21
33
  // src/pr-review/gemini/gemini-commander.ts
22
34
  var args = process.argv.slice(2);
35
+ var trace = createTraceLogger("gemini-commander", args);
23
36
  var createGeminiCommand = (tempDiffPath, reviewFormPath2) => {
37
+ trace("createGeminiCommand:start", `tempDiffPath=${tempDiffPath}, reviewFormPath=${reviewFormPath2}`);
24
38
  let modelOption = "";
25
39
  if (args.includes("--review")) {
26
40
  modelOption = "--model pro";
41
+ trace("model:review", modelOption);
27
42
  } else if (args.includes("--flash")) {
28
43
  modelOption = "--model flash";
44
+ trace("model:flash", modelOption);
29
45
  } else {
30
46
  const modelIndex = args.indexOf("--model");
31
47
  if (modelIndex !== -1 && args[modelIndex + 1]) {
32
48
  console.warn("\u26A0\uFE0F \uC9C0\uC815\uD55C \uBAA8\uB378\uC774 \uC5C6\uB294 \uACBD\uC6B0, \uC5D0\uB7EC\uAC00 \uBC1C\uC0DD\uD558\uB2C8 \uC8FC\uC758\uD558\uC138\uC694.");
33
49
  modelOption = `--model ${args[modelIndex + 1]}`;
50
+ trace("model:custom", modelOption);
34
51
  } else {
35
52
  console.warn("\u26A0\uFE0F \uBAA8\uB378\uC774 \uC9C0\uC815\uB418\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4. \uAE30\uBCF8 \uBAA8\uB378\uC778 gemini-flash\uB97C \uC0AC\uC6A9\uD569\uB2C8\uB2E4.");
36
53
  modelOption = "--model flash";
54
+ trace("model:default", modelOption);
37
55
  }
38
56
  }
39
57
  const rules = [
@@ -42,14 +60,19 @@ var createGeminiCommand = (tempDiffPath, reviewFormPath2) => {
42
60
  { path: codingConventionRulesPath, display: "\uCF54\uB529 \uCEE8\uBCA4\uC158" }
43
61
  ];
44
62
  const validRules = rules.filter((rule) => fs__default.default.existsSync(rule.path)).map((rule) => `@${rule.path}`).join(", ");
63
+ const rulesCount = validRules ? validRules.split(",").length : 0;
64
+ trace("rules:loaded", `count=${rulesCount}`);
45
65
  const command = `gemini ${modelOption} -p "\uB2E4\uC74C \uADDC\uCE59\uB4E4\uC744 \uCC38\uACE0\uD574\uC11C(${validRules}) \uC774 diff(@${tempDiffPath})\uB97C \uB9AC\uBDF0\uD574\uC918. \uB9AC\uBDF0\uC591\uC2DD\uC740 @${reviewFormPath2} \uC5D0 \uB9DE\uCDB0\uC11C \uC791\uC131\uD574\uC918. "`;
66
+ trace("command:created");
46
67
  if (args.includes("--test")) {
47
68
  const safeCommand = command.replace(/"/g, '\\"');
69
+ trace("test-mode:return-preview");
48
70
  return `echo "[TEST MODE] Gemini \uBA85\uB839\uC5B4\uAC00 \uC2E4\uD589\uB418\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4.
49
71
 
50
72
  \uC0DD\uC131\uB420 \uBA85\uB839\uC5B4 \uBBF8\uB9AC\uBCF4\uAE30:
51
73
  ${safeCommand}"`;
52
74
  }
75
+ trace("createGeminiCommand:end");
53
76
  return command;
54
77
  };
55
78