sales-frontend-gemini-cli 0.4.1 โ†’ 0.4.3

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 (45) hide show
  1. package/dist/common/helper.cjs +674 -68
  2. package/dist/common/helper.cjs.map +1 -1
  3. package/dist/common/helper.d.cts +106 -4
  4. package/dist/common/helper.d.ts +106 -4
  5. package/dist/common/helper.js +659 -70
  6. package/dist/common/helper.js.map +1 -1
  7. package/dist/common/types.d.cts +24 -1
  8. package/dist/common/types.d.ts +24 -1
  9. package/dist/pr-review/claude/claude-commander.cjs +58 -10
  10. package/dist/pr-review/claude/claude-commander.cjs.map +1 -1
  11. package/dist/pr-review/claude/claude-commander.js +58 -10
  12. package/dist/pr-review/claude/claude-commander.js.map +1 -1
  13. package/dist/pr-review/claude/installation-claude.cjs +219 -13
  14. package/dist/pr-review/claude/installation-claude.cjs.map +1 -1
  15. package/dist/pr-review/claude/installation-claude.js +218 -13
  16. package/dist/pr-review/claude/installation-claude.js.map +1 -1
  17. package/dist/pr-review/codex/codex-commander.cjs +55 -9
  18. package/dist/pr-review/codex/codex-commander.cjs.map +1 -1
  19. package/dist/pr-review/codex/codex-commander.js +55 -9
  20. package/dist/pr-review/codex/codex-commander.js.map +1 -1
  21. package/dist/pr-review/codex/installation-codex.cjs +219 -13
  22. package/dist/pr-review/codex/installation-codex.cjs.map +1 -1
  23. package/dist/pr-review/codex/installation-codex.js +218 -13
  24. package/dist/pr-review/codex/installation-codex.js.map +1 -1
  25. package/dist/pr-review/gemini/gemini-commander.cjs +82 -16
  26. package/dist/pr-review/gemini/gemini-commander.cjs.map +1 -1
  27. package/dist/pr-review/gemini/gemini-commander.js +82 -16
  28. package/dist/pr-review/gemini/gemini-commander.js.map +1 -1
  29. package/dist/pr-review/gemini/installation-gemini.cjs +219 -13
  30. package/dist/pr-review/gemini/installation-gemini.cjs.map +1 -1
  31. package/dist/pr-review/gemini/installation-gemini.js +218 -13
  32. package/dist/pr-review/gemini/installation-gemini.js.map +1 -1
  33. package/dist/pr-review/review-one-by-one.cjs +838 -184
  34. package/dist/pr-review/review-one-by-one.cjs.map +1 -1
  35. package/dist/pr-review/review-one-by-one.js +839 -185
  36. package/dist/pr-review/review-one-by-one.js.map +1 -1
  37. package/dist/pr-review/review.cjs +815 -156
  38. package/dist/pr-review/review.cjs.map +1 -1
  39. package/dist/pr-review/review.js +815 -156
  40. package/dist/pr-review/review.js.map +1 -1
  41. package/package.json +4 -7
  42. package/src/common/rules/coding-convention.md +393 -0
  43. package/src/common/rules/coding-convention.pdf +0 -0
  44. package/src/common/rules/naming-rule.md +347 -0
  45. package/src/common/rules/naming-rule.pdf +0 -0
@@ -1,26 +1,229 @@
1
1
  import { execSync } from 'child_process';
2
+ import fs from 'fs';
2
3
  import path from 'path';
3
4
  import { fileURLToPath } from 'url';
5
+ import { inspect } from 'util';
4
6
 
5
7
  // src/pr-review/gemini/installation-gemini.ts
6
8
  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");
9
+ var traceMessages = [];
10
+ var GEMINI_CLI_PACKAGE_NAME = "sales-frontend-gemini-cli";
11
+ var cachedPackageRootPath = "";
12
+ function isGeminiCliPackageRoot(directory) {
13
+ const packageJsonPath = path.join(directory, "package.json");
14
+ if (!fs.existsSync(packageJsonPath)) {
15
+ return false;
16
+ }
17
+ try {
18
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
19
+ return packageJson.name === GEMINI_CLI_PACKAGE_NAME;
20
+ } catch {
21
+ return false;
22
+ }
23
+ }
24
+ function resolveGeminiCliPackageRoot(startDirectory = __dirname) {
25
+ if (cachedPackageRootPath) {
26
+ return cachedPackageRootPath;
27
+ }
28
+ let currentDirectory = startDirectory;
29
+ while (true) {
30
+ if (isGeminiCliPackageRoot(currentDirectory)) {
31
+ cachedPackageRootPath = currentDirectory;
32
+ return cachedPackageRootPath;
33
+ }
34
+ const parentDirectory = path.dirname(currentDirectory);
35
+ if (parentDirectory === currentDirectory) {
36
+ break;
37
+ }
38
+ currentDirectory = parentDirectory;
39
+ }
40
+ cachedPackageRootPath = path.resolve(startDirectory, "../..");
41
+ return cachedPackageRootPath;
42
+ }
43
+ function resolvePackageAssetPath(relativeFilePath) {
44
+ return path.resolve(resolveGeminiCliPackageRoot(), relativeFilePath);
45
+ }
46
+ resolvePackageAssetPath("src/common/rules/review-rules.md");
47
+ resolvePackageAssetPath("src/common/rules/naming-rule.md");
48
+ resolvePackageAssetPath("src/common/rules/coding-convention.md");
49
+ resolvePackageAssetPath("src/common/form/review-form.md");
50
+ resolvePackageAssetPath("src/common/form/review-form-one-by-one.md");
51
+ var REPORT_DIR = ".review-report";
12
52
  function isTestMode(args = process.argv.slice(2)) {
13
53
  return args.includes("--test");
14
54
  }
55
+ function getTraceMessages() {
56
+ return [...traceMessages];
57
+ }
15
58
  function createTraceLogger(scope, args = process.argv.slice(2)) {
16
59
  const enabled = isTestMode(args);
17
60
  return (step, detail) => {
61
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
62
+ const message = `[${timestamp}][TRACE][${scope}] ${step}${detail ? ` | ${detail}` : ""}`;
63
+ traceMessages.push(message);
18
64
  if (!enabled) {
19
65
  return;
20
66
  }
21
- console.log(`[TRACE][${scope}] ${step}${detail ? ` | ${detail}` : ""}`);
67
+ console.log(message);
68
+ };
69
+ }
70
+ var helperTrace = createTraceLogger("helper");
71
+ function getTimestampParts(now = /* @__PURE__ */ new Date()) {
72
+ return {
73
+ YYYY: now.getFullYear(),
74
+ MM: String(now.getMonth() + 1).padStart(2, "0"),
75
+ DD: String(now.getDate()).padStart(2, "0"),
76
+ HH: String(now.getHours()).padStart(2, "0"),
77
+ mm: String(now.getMinutes()).padStart(2, "0"),
78
+ ss: String(now.getSeconds()).padStart(2, "0")
22
79
  };
23
80
  }
81
+ function getHumanReadableNowString(now = /* @__PURE__ */ new Date()) {
82
+ const { YYYY, MM, DD, HH, mm, ss } = getTimestampParts(now);
83
+ return `${YYYY}-${MM}-${DD} ${HH}:${mm}:${ss}`;
84
+ }
85
+ function stringifyUnknown(value) {
86
+ if (value === void 0 || value === null) {
87
+ return "";
88
+ }
89
+ if (typeof value === "string") {
90
+ return value;
91
+ }
92
+ if (Buffer.isBuffer(value)) {
93
+ return value.toString();
94
+ }
95
+ if (value instanceof Error) {
96
+ return value.stack || value.message;
97
+ }
98
+ return inspect(value, { depth: 5, breakLength: 120 });
99
+ }
100
+ function getErrorSummary(error) {
101
+ if (error instanceof Error) {
102
+ return `${error.name}: ${error.message}`;
103
+ }
104
+ return stringifyUnknown(error) || "Unknown error";
105
+ }
106
+ function serializeError(error) {
107
+ const serialized = {
108
+ summary: getErrorSummary(error)
109
+ };
110
+ if (error instanceof Error) {
111
+ serialized.name = error.name;
112
+ serialized.message = error.message;
113
+ serialized.stack = error.stack;
114
+ } else {
115
+ serialized.value = stringifyUnknown(error);
116
+ }
117
+ if (error && typeof error === "object") {
118
+ const errorLike = error;
119
+ const extraKeys = ["code", "errno", "syscall", "path", "cmd", "status", "signal", "spawnargs"];
120
+ extraKeys.forEach((key) => {
121
+ if (errorLike[key] !== void 0) {
122
+ serialized[key] = errorLike[key];
123
+ }
124
+ });
125
+ const stdout = stringifyUnknown(errorLike.stdout);
126
+ if (stdout) {
127
+ serialized.stdout = stdout;
128
+ }
129
+ const stderr = stringifyUnknown(errorLike.stderr);
130
+ if (stderr) {
131
+ serialized.stderr = stderr;
132
+ }
133
+ const cause = stringifyUnknown(errorLike.cause);
134
+ if (cause) {
135
+ serialized.cause = cause;
136
+ }
137
+ }
138
+ return serialized;
139
+ }
140
+ function getNextFilePath(dir, baseName, extension) {
141
+ let counter = 1;
142
+ while (true) {
143
+ const filePath = path.join(dir, `${baseName}-${counter}${extension}`);
144
+ if (!fs.existsSync(filePath)) {
145
+ return filePath;
146
+ }
147
+ counter++;
148
+ }
149
+ }
150
+ function getAvailableFilePath(dir, baseName, extension) {
151
+ const firstFilePath = path.join(dir, `${baseName}${extension}`);
152
+ if (!fs.existsSync(firstFilePath)) {
153
+ return firstFilePath;
154
+ }
155
+ return getNextFilePath(dir, baseName, extension);
156
+ }
157
+ function createReportDirectory() {
158
+ if (!fs.existsSync(REPORT_DIR)) {
159
+ fs.mkdirSync(REPORT_DIR, { recursive: true });
160
+ }
161
+ }
162
+ function getErrorLogTimestamp(now = /* @__PURE__ */ new Date()) {
163
+ const { YYYY, MM, DD, HH, mm, ss } = getTimestampParts(now);
164
+ return `${YYYY}-${MM}-${DD}-${HH}\uC2DC-${mm}\uBD84-${ss}\uCD08`;
165
+ }
166
+ function writeErrorReport(error, options = {}) {
167
+ try {
168
+ const now = /* @__PURE__ */ new Date();
169
+ helperTrace("error-report:write:start", options.scope || "unknown");
170
+ createReportDirectory();
171
+ const reportPath = getAvailableFilePath(REPORT_DIR, `error-log-${getErrorLogTimestamp(now)}`, ".md");
172
+ const serializedError = serializeError(error);
173
+ const traceSnapshot = options.traceMessages ?? getTraceMessages();
174
+ const extraSections = options.extraSections || [];
175
+ const report = `# Error Log
176
+
177
+ - \uBC1C\uC0DD \uC2DC\uAC01: ${getHumanReadableNowString(now)}
178
+ - Scope: \`${options.scope || "unknown"}\`
179
+ - \uC791\uC5C5 \uACBD\uB85C: \`${process.cwd()}\`
180
+ - \uC2E4\uD589 \uC778\uC790: \`${JSON.stringify(options.args ?? process.argv.slice(2))}\`
181
+ - \uC2E4\uD589 \uD658\uACBD: \`${process.platform} ${process.arch} / Node ${process.version}\`
182
+
183
+ ## Summary
184
+
185
+ ${options.title || serializedError.summary || "Unknown error"}
186
+
187
+ ## Error
188
+
189
+ \`\`\`json
190
+ ${JSON.stringify(serializedError, null, 2)}
191
+ \`\`\`
192
+
193
+ ## Trace
194
+
195
+ \`\`\`json
196
+ ${JSON.stringify(traceSnapshot, null, 2)}
197
+ \`\`\`${extraSections.length ? `
198
+ ${extraSections.map((section) => `
199
+ ## ${section.heading}
200
+
201
+ ${section.markdown}`).join("\n")}
202
+ ` : "\n"}
203
+ `;
204
+ fs.writeFileSync(reportPath, report);
205
+ helperTrace("error-report:write:done", reportPath);
206
+ return reportPath;
207
+ } catch (writeError) {
208
+ console.error("\u26A0\uFE0F \uC5D0\uB7EC \uB85C\uADF8 \uD30C\uC77C \uC0DD\uC131\uC5D0 \uC2E4\uD328\uD588\uC2B5\uB2C8\uB2E4.");
209
+ console.error(writeError);
210
+ return "";
211
+ }
212
+ }
213
+ function exitWithError(message, options = {}) {
214
+ const reportPath = writeErrorReport(options.error || new Error(message), {
215
+ ...options,
216
+ title: message
217
+ });
218
+ console.error(message);
219
+ if (options.error) {
220
+ console.error(options.error);
221
+ }
222
+ if (reportPath) {
223
+ console.error(`\u{1F4C4} \uC5D0\uB7EC \uB85C\uADF8 \uC800\uC7A5 \uC704\uCE58: ${reportPath}`);
224
+ }
225
+ process.exit(1);
226
+ }
24
227
 
25
228
  // src/pr-review/gemini/installation-gemini.ts
26
229
  var trace = createTraceLogger("installation-gemini");
@@ -30,21 +233,23 @@ function checkGeminiCliInstalled() {
30
233
  trace("version-check:run", "gemini --version");
31
234
  execSync("gemini --version", { stdio: "ignore" });
32
235
  trace("version-check:ok");
33
- } catch {
34
- trace("version-check:failed", "install-start");
236
+ } catch (error) {
237
+ trace("version-check:failed", getErrorSummary(error));
238
+ trace("install:start", "@google/gemini-cli");
35
239
  console.log("\u2139\uFE0F gemini-cli\uAC00 \uC124\uCE58\uB418\uC5B4 \uC788\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4. \uC124\uCE58\uB97C \uC9C4\uD589\uD569\uB2C8\uB2E4... npm install -g @google/gemini-cli");
36
240
  try {
37
241
  execSync("npm install -g @google/gemini-cli", { stdio: "inherit" });
38
- trace("install:ok", "exit(1) for login");
242
+ trace("install:ok", "login-required");
39
243
  console.log("\u2705 gemini-cli \uC124\uCE58\uAC00 \uC644\uB8CC\uB418\uC5C8\uC2B5\uB2C8\uB2E4.");
40
244
  console.log("\u26A0\uFE0F Gemini API \uC0AC\uC6A9\uC744 \uC704\uD574 \uC778\uC99D\uC774 \uD544\uC694\uD569\uB2C8\uB2E4.");
41
245
  console.log(' \uD130\uBBF8\uB110\uC5D0\uC11C "gemini" \uB97C \uC785\uB825\uD558\uC5EC \uBE0C\uB77C\uC6B0\uC800 \uB85C\uADF8\uC778\uC744 \uC644\uB8CC\uD55C \uD6C4, \uB2E4\uC2DC \uC2DC\uB3C4\uD574\uC8FC\uC138\uC694.');
42
246
  process.exit(1);
43
247
  } catch (installError) {
44
- trace("install:failed");
45
- console.error("\u274C gemini-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);
248
+ trace("install:failed", getErrorSummary(installError));
249
+ exitWithError("\u274C gemini-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).", {
250
+ scope: "installation-gemini",
251
+ error: installError
252
+ });
48
253
  }
49
254
  }
50
255
  trace("checkGeminiCliInstalled:end");
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/common/helper.ts","../../../src/pr-review/gemini/installation-gemini.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;AAsCxG,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;;;ACjEA,IAAM,KAAA,GAAQ,kBAAkB,qBAAqB,CAAA;AAG9C,SAAS,uBAA0B,GAAA;AACxC,EAAA,KAAA,CAAM,+BAA+B,CAAA;AACrC,EAAI,IAAA;AACF,IAAA,KAAA,CAAM,qBAAqB,kBAAkB,CAAA;AAC7C,IAAA,QAAA,CAAS,kBAAoB,EAAA,EAAE,KAAO,EAAA,QAAA,EAAU,CAAA;AAChD,IAAA,KAAA,CAAM,kBAAkB,CAAA;AAAA,GAClB,CAAA,MAAA;AACN,IAAA,KAAA,CAAM,wBAAwB,eAAe,CAAA;AAC7C,IAAA,OAAA,CAAQ,IAAI,sLAA6E,CAAA;AACzF,IAAI,IAAA;AACF,MAAA,QAAA,CAAS,mCAAqC,EAAA,EAAE,KAAO,EAAA,SAAA,EAAW,CAAA;AAClE,MAAA,KAAA,CAAM,cAAc,mBAAmB,CAAA;AACvC,MAAA,OAAA,CAAQ,IAAI,kFAA2B,CAAA;AACvC,MAAA,OAAA,CAAQ,IAAI,6GAAkC,CAAA;AAC9C,MAAA,OAAA,CAAQ,IAAI,4MAAsD,CAAA;AAClE,MAAA,OAAA,CAAQ,KAAK,CAAC,CAAA;AAAA,aACP,YAAc,EAAA;AACrB,MAAA,KAAA,CAAM,gBAAgB,CAAA;AACtB,MAAA,OAAA,CAAQ,MAAM,qLAAwD,CAAA;AACtE,MAAA,OAAA,CAAQ,MAAM,YAAY,CAAA;AAC1B,MAAA,OAAA,CAAQ,KAAK,CAAC,CAAA;AAAA;AAChB;AAEF,EAAA,KAAA,CAAM,6BAA6B,CAAA;AACrC","file":"installation-gemini.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\nfunction parseServiceFromArgs(args: string[] = process.argv.slice(2)): AIServiceType | '' {\n const serviceIndex = args.indexOf('--service');\n const rawService = serviceIndex !== -1 ? args[serviceIndex + 1] : '';\n\n if (!rawService) {\n return '';\n }\n\n const normalizedService = rawService.toLowerCase();\n\n if (AIServices.includes(normalizedService as AIServiceType)) {\n return normalizedService as AIServiceType;\n }\n\n console.error(\n `โŒ ์ง€์›ํ•˜์ง€ ์•Š๋Š” ์„œ๋น„์Šค์ž…๋‹ˆ๋‹ค: ${rawService}. ์‚ฌ์šฉ ๊ฐ€๋Šฅ ๊ฐ’: ${AIServices.join(', ')} (์˜ˆ: --service codex)`\n );\n process.exit(1);\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 service = parseServiceFromArgs();\n\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 const selectedServiceFromArgs = parseServiceFromArgs();\n\n if (selectedServiceFromArgs) {\n console.log(`\\nโœ… \\u001b[32m${selectedServiceFromArgs}\\u001b[0m ์„œ๋น„์Šค๊ฐ€ ์„ ํƒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. (--service)\\n`);\n\n return selectedServiceFromArgs;\n }\n\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-gemini');\n\n// gemini-cli ์„ค์น˜ ํ™•์ธ ๋ฐ ์„ค์น˜\nexport function checkGeminiCliInstalled() {\n trace('checkGeminiCliInstalled:start');\n try {\n trace('version-check:run', 'gemini --version');\n execSync('gemini --version', { stdio: 'ignore' });\n trace('version-check:ok');\n } catch {\n trace('version-check:failed', 'install-start');\n console.log('โ„น๏ธ gemini-cli๊ฐ€ ์„ค์น˜๋˜์–ด ์žˆ์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ์„ค์น˜๋ฅผ ์ง„ํ–‰ํ•ฉ๋‹ˆ๋‹ค... npm install -g @google/gemini-cli');\n try {\n execSync('npm install -g @google/gemini-cli', { stdio: 'inherit' });\n trace('install:ok', 'exit(1) for login');\n console.log('โœ… gemini-cli ์„ค์น˜๊ฐ€ ์™„๋ฃŒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.');\n console.log('โš ๏ธ Gemini API ์‚ฌ์šฉ์„ ์œ„ํ•ด ์ธ์ฆ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.');\n console.log(' ํ„ฐ๋ฏธ๋„์—์„œ \"gemini\" ๋ฅผ ์ž…๋ ฅํ•˜์—ฌ ๋ธŒ๋ผ์šฐ์ € ๋กœ๊ทธ์ธ์„ ์™„๋ฃŒํ•œ ํ›„, ๋‹ค์‹œ ์‹œ๋„ํ•ด์ฃผ์„ธ์š”.');\n process.exit(1);\n } catch (installError) {\n trace('install:failed');\n console.error('โŒ gemini-cli ์„ค์น˜ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค. ๊ถŒํ•œ ๋ฌธ์ œ์ผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค (sudo ํ•„์š”).');\n console.error(installError);\n process.exit(1);\n }\n }\n trace('checkGeminiCliInstalled:end');\n}\n"]}
1
+ {"version":3,"sources":["../../../src/common/helper.ts","../../../src/pr-review/gemini/installation-gemini.ts"],"names":[],"mappings":"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","file":"installation-gemini.js","sourcesContent":["import { execFileSync, execSync } from 'child_process';\nimport fs from 'fs';\nimport path from 'path';\nimport readline from 'readline';\nimport { fileURLToPath } from 'url';\nimport { inspect } from 'util';\n\nimport { AIServiceType, CommitOption, MultiSelectOption } from './types';\n\ntype ErrorReportSection = {\n heading: string;\n markdown: string;\n};\n\ntype WriteErrorReportOptions = {\n title?: string;\n scope?: string;\n args?: string[];\n traceMessages?: string[];\n extraSections?: ErrorReportSection[];\n};\n\ntype GitCommandOptions = {\n allowFailure?: boolean;\n trimOutput?: boolean;\n};\n\ntype ShellCommandProgressOptions = {\n progressIntervalMs?: number;\n progressMessage?: string;\n streamOutput?: boolean;\n};\n\nconst __dirname = path.dirname(fileURLToPath(import.meta.url));\nconst traceMessages: string[] = [];\nconst GEMINI_CLI_PACKAGE_NAME = 'sales-frontend-gemini-cli';\nlet cachedPackageRootPath = '';\n\n/**\n * @description\n * ํ˜„์žฌ ๋””๋ ‰ํ„ฐ๋ฆฌ๊ฐ€ gemini-cli ํŒจํ‚ค์ง€ ๋ฃจํŠธ์ธ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค.\n * ๋ฒˆ๋“ค ๊ฒฐ๊ณผ๋ฌผ์—์„œ helper ์ฝ”๋“œ๊ฐ€ ๊ฐ ์—”ํŠธ๋ฆฌ ํŒŒ์ผ๋กœ ์ธ๋ผ์ธ๋˜๋ฉด `__dirname` ๊ธฐ์ค€์ ์ด ๋‹ฌ๋ผ์ง€๋ฏ€๋กœ,\n * package.json ์ด๋ฆ„์œผ๋กœ ์‹ค์ œ ํŒจํ‚ค์ง€ ๋ฃจํŠธ๋ฅผ ์‹๋ณ„ํ•ด์•ผ ๊ทœ์น™/์–‘์‹ ํŒŒ์ผ ๊ฒฝ๋กœ๊ฐ€ ๊นจ์ง€์ง€ ์•Š์Šต๋‹ˆ๋‹ค.\n * @param directory ๊ฒ€์‚ฌํ•  ๋””๋ ‰ํ„ฐ๋ฆฌ ๊ฒฝ๋กœ\n * @returns ํ˜„์žฌ ๋””๋ ‰ํ„ฐ๋ฆฌ๊ฐ€ gemini-cli ํŒจํ‚ค์ง€ ๋ฃจํŠธ์ด๋ฉด true\n */\nfunction isGeminiCliPackageRoot(directory: string) {\n const packageJsonPath = path.join(directory, 'package.json');\n\n if (!fs.existsSync(packageJsonPath)) {\n return false;\n }\n\n try {\n const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')) as { name?: string };\n\n return packageJson.name === GEMINI_CLI_PACKAGE_NAME;\n } catch {\n return false;\n }\n}\n\n/**\n * @description\n * ํ˜„์žฌ ์‹คํ–‰ ์ค‘์ธ ๋ฒˆ๋“ค ์—”ํŠธ๋ฆฌ ๊ธฐ์ค€์œผ๋กœ gemini-cli ํŒจํ‚ค์ง€ ๋ฃจํŠธ๋ฅผ ์ฐพ์Šต๋‹ˆ๋‹ค.\n * `dist/pr-review/review.js`, `dist/pr-review/gemini/gemini-commander.js`์ฒ˜๋Ÿผ\n * ์—”ํŠธ๋ฆฌ ์œ„์น˜๊ฐ€ ๋‹ฌ๋ผ๋„ ์ƒ์œ„ ๋””๋ ‰ํ„ฐ๋ฆฌ๋ฅผ ์ˆœํšŒํ•˜๋ฉฐ package root๋ฅผ ์ฐพ๋„๋ก ์„ค๊ณ„ํ•ฉ๋‹ˆ๋‹ค.\n * @param startDirectory ํŒจํ‚ค์ง€ ๋ฃจํŠธ ํƒ์ƒ‰์„ ์‹œ์ž‘ํ•  ๋””๋ ‰ํ„ฐ๋ฆฌ\n * @returns ๊ทœ์น™/์–‘์‹ ํŒŒ์ผ์ด ์กด์žฌํ•˜๋Š” gemini-cli ํŒจํ‚ค์ง€ ๋ฃจํŠธ ๊ฒฝ๋กœ\n */\nfunction resolveGeminiCliPackageRoot(startDirectory: string = __dirname) {\n if (cachedPackageRootPath) {\n return cachedPackageRootPath;\n }\n\n let currentDirectory = startDirectory;\n\n while (true) {\n if (isGeminiCliPackageRoot(currentDirectory)) {\n cachedPackageRootPath = currentDirectory;\n\n return cachedPackageRootPath;\n }\n\n const parentDirectory = path.dirname(currentDirectory);\n\n if (parentDirectory === currentDirectory) {\n break;\n }\n\n currentDirectory = parentDirectory;\n }\n\n /**\n * @description\n * package.json ํƒ์ƒ‰์ด ์‹คํŒจํ•ด๋„ ๊ธฐ์กด ์„ค์น˜ ๊ตฌ์กฐ(dist/common -> package root)๋ฅผ ์šฐ์„  fallback์œผ๋กœ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.\n * ์˜ˆ์ƒ์น˜ ๋ชปํ•œ ๋ฐฐํฌ ํ™˜๊ฒฝ์—์„œ๋„ ์ตœ๋Œ€ํ•œ ๊ธฐ์กด ๋™์ž‘์„ ์œ ์ง€ํ•˜๊ธฐ ์œ„ํ•œ ๋ณด์ˆ˜์  ์•ˆ์ „์žฅ์น˜์ž…๋‹ˆ๋‹ค.\n */\n cachedPackageRootPath = path.resolve(startDirectory, '../..');\n\n return cachedPackageRootPath;\n}\n\n/**\n * @description\n * gemini-cli ํŒจํ‚ค์ง€ ๋ฃจํŠธ๋ฅผ ๊ธฐ์ค€์œผ๋กœ ์ •์  asset ํŒŒ์ผ์˜ ์ ˆ๋Œ€ ๊ฒฝ๋กœ๋ฅผ ๊ณ„์‚ฐํ•ฉ๋‹ˆ๋‹ค.\n * ๋ฒˆ๋“ค ๊ณผ์ •์—์„œ helper ์ฝ”๋“œ๊ฐ€ ๋‹ค๋ฅธ ์—”ํŠธ๋ฆฌ์— ์ธ๋ผ์ธ๋˜์–ด๋„ ํ•ญ์ƒ ๊ฐ™์€ ์‹ค์ œ ํŒŒ์ผ์„ ๊ฐ€๋ฆฌํ‚ค๊ฒŒ ํ•ฉ๋‹ˆ๋‹ค.\n * @param relativeFilePath ํŒจํ‚ค์ง€ ๋ฃจํŠธ ๊ธฐ์ค€ ์ƒ๋Œ€ ๊ฒฝ๋กœ\n * @returns asset ํŒŒ์ผ์˜ ์ ˆ๋Œ€ ๊ฒฝ๋กœ\n */\nfunction resolvePackageAssetPath(relativeFilePath: string) {\n return path.resolve(resolveGeminiCliPackageRoot(), relativeFilePath);\n}\n\nexport const rulesPath = resolvePackageAssetPath('src/common/rules/review-rules.md');\nexport const namingRulesPath = resolvePackageAssetPath('src/common/rules/naming-rule.md');\nexport const codingConventionRulesPath = resolvePackageAssetPath('src/common/rules/coding-convention.md');\nexport const reviewFormPath = resolvePackageAssetPath('src/common/form/review-form.md');\nexport const reviewFormOneByOnePath = resolvePackageAssetPath('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 COMMIT_FETCH_LIMIT = 20;\nexport const COMMIT_SELECTION_WINDOW = 8;\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 clearTraceMessages() {\n traceMessages.length = 0;\n}\n\nexport function getTraceMessages() {\n return [...traceMessages];\n}\n\n/**\n * @description\n * ํ„ฐ๋ฏธ๋„ ์„ ํƒ UI์—์„œ ๋™์ผํ•œ ์ƒ‰์ƒ ํ† ํฐ์„ ์žฌ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋„๋ก ANSI escape code๋ฅผ ์ƒ์ˆ˜ํ™”ํ•ฉ๋‹ˆ๋‹ค.\n * ์„œ๋น„์Šค ์„ ํƒ๊ณผ ์ปค๋ฐ‹ ์„ ํƒ์ด ๊ฐ™์€ ์‹œ๊ฐ ์–ธ์–ด๋ฅผ ์‚ฌ์šฉํ•˜๋„๋ก helper ๋‚ด๋ถ€์—์„œ๋งŒ ๊ณต์œ ํ•ฉ๋‹ˆ๋‹ค.\n */\nconst ANSI = {\n bold: '\\u001b[1m',\n cyan: '\\u001b[36m',\n dim: '\\u001b[2m',\n green: '\\u001b[32m',\n reset: '\\u001b[0m',\n yellow: '\\u001b[33m'\n} as const;\nconst ANSI_PATTERN = new RegExp(`${String.fromCharCode(27)}\\\\[[0-9;]*m`, 'g');\nconst COMBINING_MARK_PATTERN = /\\p{Mark}/u;\nconst GRAPHEME_SEGMENTER =\n typeof Intl !== 'undefined' && 'Segmenter' in Intl ? new Intl.Segmenter('ko', { granularity: 'grapheme' }) : null;\n\ntype VisibleToken = {\n value: string;\n visibleWidth: number;\n};\n\n/**\n * @description\n * ๋ฆฌ๋ทฐ ๋Œ€์ƒ ํŒŒ์ผ ํ•„ํ„ฐ๋ฅผ pathspec ๋ฐฐ์—ด ํ˜•ํƒœ๋กœ ์ •์˜ํ•ฉ๋‹ˆ๋‹ค.\n * ๋ฌธ์ž์—ด ์ปค๋งจ๋“œ์™€ argv ๊ธฐ๋ฐ˜ git ์‹คํ–‰์ด ๋ชจ๋‘ ๊ฐ™์€ ๊ธฐ์ค€์„ ๊ณต์œ ํ•˜๋„๋ก ์›๋ณธ ํŒจํ„ด์€ ๋ฐฐ์—ด๋กœ ์œ ์ง€ํ•ฉ๋‹ˆ๋‹ค.\n */\nfunction getGitDiffPathspecs() {\n return {\n excludePatterns: ignoreList.map((item) => `:(exclude)${item}`),\n includePatterns: ['*.ts', '*.tsx', '*.js', '*.jsx']\n };\n}\n\nfunction segmentGraphemes(value: string) {\n if (!GRAPHEME_SEGMENTER) {\n return [...value];\n }\n\n return [...GRAPHEME_SEGMENTER.segment(value)].map(({ segment }) => segment);\n}\n\nfunction isWideCodePoint(codePoint: number) {\n return (\n codePoint >= 0x1100 &&\n (codePoint <= 0x115f ||\n codePoint === 0x2329 ||\n codePoint === 0x232a ||\n (codePoint >= 0x2e80 && codePoint <= 0x3247 && codePoint !== 0x303f) ||\n (codePoint >= 0x3250 && codePoint <= 0x4dbf) ||\n (codePoint >= 0x4e00 && codePoint <= 0xa4c6) ||\n (codePoint >= 0xa960 && codePoint <= 0xa97c) ||\n (codePoint >= 0xac00 && codePoint <= 0xd7a3) ||\n (codePoint >= 0xf900 && codePoint <= 0xfaff) ||\n (codePoint >= 0xfe10 && codePoint <= 0xfe19) ||\n (codePoint >= 0xfe30 && codePoint <= 0xfe6b) ||\n (codePoint >= 0xff01 && codePoint <= 0xff60) ||\n (codePoint >= 0xffe0 && codePoint <= 0xffe6) ||\n (codePoint >= 0x1f200 && codePoint <= 0x1f251) ||\n (codePoint >= 0x20000 && codePoint <= 0x3fffd))\n );\n}\n\nfunction isEmojiCodePoint(codePoint: number) {\n return (\n (codePoint >= 0x1f1e6 && codePoint <= 0x1f1ff) ||\n (codePoint >= 0x1f300 && codePoint <= 0x1faff) ||\n (codePoint >= 0x2600 && codePoint <= 0x27bf)\n );\n}\n\nfunction getGraphemeWidth(grapheme: string) {\n let width = 0;\n\n for (const character of grapheme) {\n const codePoint = character.codePointAt(0);\n\n if (!codePoint || COMBINING_MARK_PATTERN.test(character) || codePoint === 0x200d) {\n continue;\n }\n\n if ((codePoint >= 0xfe00 && codePoint <= 0xfe0f) || (codePoint >= 0xe0100 && codePoint <= 0xe01ef)) {\n continue;\n }\n\n if (isWideCodePoint(codePoint) || isEmojiCodePoint(codePoint)) {\n width = Math.max(width, 2);\n\n continue;\n }\n\n width = Math.max(width, 1);\n }\n\n return width;\n}\n\nfunction tokenizePlainText(value: string) {\n return segmentGraphemes(value).map((segment) => ({\n value: segment,\n visibleWidth: getGraphemeWidth(segment)\n }));\n}\n\nfunction tokenizeVisibleText(value: string) {\n const tokens: VisibleToken[] = [];\n let lastIndex = 0;\n\n for (const match of value.matchAll(ANSI_PATTERN)) {\n const index = match.index ?? 0;\n\n if (index > lastIndex) {\n tokens.push(...tokenizePlainText(value.slice(lastIndex, index)));\n }\n\n tokens.push({\n value: match[0],\n visibleWidth: 0\n });\n lastIndex = index + match[0].length;\n }\n\n if (lastIndex < value.length) {\n tokens.push(...tokenizePlainText(value.slice(lastIndex)));\n }\n\n return tokens;\n}\n\n/**\n * @description\n * ๋ชจ๋‹ฌ ๊ฐ ์ค„์ด ํ„ฐ๋ฏธ๋„ ์‹ค์ œ ํญ์„ ๋„˜์ง€ ์•Š๋„๋ก ANSI ์ฝ”๋“œ๋ฅผ ๋ณด์กดํ•œ ์ฑ„ ์ž˜๋ผ๋ƒ…๋‹ˆ๋‹ค.\n * ์ค„๋ฐ”๊ฟˆ์ด ๋ฐœ์ƒํ•˜๋ฉด ๋ Œ๋” ์ค„ ์ˆ˜ ๊ณ„์‚ฐ์ด ์–ด๊ธ‹๋‚˜๋ฏ€๋กœ, ๋ชจ๋“  ์„ ํƒ ์ค„์„ 1 physical line๋กœ ๊ฐ•์ œํ•ฉ๋‹ˆ๋‹ค.\n * @param value ๋ Œ๋”ํ•  ๋ฌธ์ž์—ด\n * @param maxWidth ํ—ˆ์šฉ ์ตœ๋Œ€ ํ‘œ์‹œ ํญ\n * @returns ํ„ฐ๋ฏธ๋„ ํญ ์•ˆ์œผ๋กœ ์ •๋ฆฌ๋œ ๋ฌธ์ž์—ด\n */\nfunction truncateLineForTerminal(value: string, maxWidth: number) {\n if (maxWidth <= 0) {\n return '';\n }\n\n const tokens = tokenizeVisibleText(value);\n const totalWidth = tokens.reduce((sum, token) => sum + token.visibleWidth, 0);\n\n if (totalWidth <= maxWidth) {\n return value;\n }\n\n const ellipsis = '...';\n const ellipsisWidth = 3;\n const targetWidth = Math.max(0, maxWidth - ellipsisWidth);\n let usedWidth = 0;\n let result = '';\n\n for (const token of tokens) {\n if (token.visibleWidth === 0) {\n result += token.value;\n\n continue;\n }\n\n if (usedWidth + token.visibleWidth > targetWidth) {\n break;\n }\n\n result += token.value;\n usedWidth += token.visibleWidth;\n }\n\n return `${result}${ellipsis}${ANSI.reset}`;\n}\n\nfunction fitLinesToTerminal(lines: string[]) {\n const maxWidth = Math.max(20, (process.stdout.columns || 120) - 1);\n\n return lines.map((line) => truncateLineForTerminal(line, maxWidth));\n}\n\n/**\n * @description\n * git ์„œ๋ธŒํ”„๋กœ์„ธ์Šค๋ฅผ argv ๋ฐฐ์—ด ๊ธฐ๋ฐ˜์œผ๋กœ ์‹คํ–‰ํ•ฉ๋‹ˆ๋‹ค.\n * ์ปค๋ฐ‹ ํ•ด์‹œ๋‚˜ ํŒŒ์ผ ๊ฒฝ๋กœ์— ๊ณต๋ฐฑ์ด ์„ž์—ฌ๋„ shell quoting ๋ฌธ์ œ ์—†์ด ๋™์ผํ•œ ๋™์ž‘์„ ๋ณด์žฅํ•˜๊ธฐ ์œ„ํ•ด ๊ณตํ†ต helper๋กœ ๋ถ„๋ฆฌํ•ฉ๋‹ˆ๋‹ค.\n * @param args git ์ธ์ž ๋ฐฐ์—ด\n * @param options ์‹คํŒจ ํ—ˆ์šฉ ์—ฌ๋ถ€์™€ ์ถœ๋ ฅ trim ์—ฌ๋ถ€\n * @returns git ํ‘œ์ค€์ถœ๋ ฅ ๋ฌธ์ž์—ด\n */\nfunction runGitCommand(args: string[], options: GitCommandOptions = {}) {\n const { allowFailure = false, trimOutput = true } = options;\n\n try {\n const output = execFileSync('git', args, {\n encoding: 'utf8',\n maxBuffer: 1024 * 1024 * 20,\n stdio: ['ignore', 'pipe', 'pipe']\n });\n\n return trimOutput ? output.trim() : output;\n } catch (error) {\n helperTrace('git-command:failed', `${args.join(' ')} | ${getErrorSummary(error)}`);\n\n if (allowFailure) {\n return '';\n }\n\n throw error;\n }\n}\n\n/**\n * @description\n * shell ๋ช…๋ น์„ ๋น„๋™๊ธฐ๋กœ ์‹คํ–‰ํ•˜๋ฉด์„œ stdout/stderr๋ฅผ ๊ทธ๋Œ€๋กœ ํ˜๋ ค๋ณด๋‚ด๊ณ , ์žฅ์‹œ๊ฐ„ ์ž‘์—…์—๋Š” ๊ฒฝ๊ณผ ์‹œ๊ฐ„์„ ์ฃผ๊ธฐ์ ์œผ๋กœ ์ถœ๋ ฅํ•ฉ๋‹ˆ๋‹ค.\n * review ์‹คํ–‰์ฒ˜๋Ÿผ ์‘๋‹ต ์ƒ์„ฑ๊นŒ์ง€ ์˜ค๋ž˜ ๊ฑธ๋ฆฌ๋Š” CLI๋ฅผ ๋ฒ„ํผ๋ง ์—†์ด ๊ธฐ๋‹ค๋ฆฌ๊ฒŒ ํ•ด ์‚ฌ์šฉ์ž๊ฐ€ ๋ฉˆ์ถค์œผ๋กœ ์˜คํ•ดํ•˜์ง€ ์•Š๋„๋ก ํ•ฉ๋‹ˆ๋‹ค.\n * @param command ์‹คํ–‰ํ•  shell command ๋ฌธ์ž์—ด\n * @param options ์ง„ํ–‰ ๋ฉ”์‹œ์ง€์™€ ์ถœ๋ ฅ ์ฃผ๊ธฐ ์˜ต์…˜\n * @returns ์ตœ์ข… stdout/stderr ์บก์ฒ˜ ๊ฒฐ๊ณผ\n */\nexport async function executeShellCommandWithProgress(command: string, options: ShellCommandProgressOptions = {}) {\n const { progressIntervalMs = 10000, progressMessage = 'โณ ๋ช…๋ น์„ ์‹คํ–‰ํ•˜๋Š” ์ค‘์ž…๋‹ˆ๋‹ค...', streamOutput = false } = options;\n const { spawn } = await import('child_process');\n\n return new Promise<{ stderr: string; stdout: string }>((resolve, reject) => {\n let stdout = '';\n let stderr = '';\n const startedAt = Date.now();\n console.log(progressMessage);\n\n const child = spawn('/bin/zsh', ['-lc', command], {\n stdio: ['ignore', 'pipe', 'pipe']\n });\n\n const progressTimer = setInterval(() => {\n const elapsedSeconds = Math.max(1, Math.floor((Date.now() - startedAt) / 1000));\n console.log(`${progressMessage} (${elapsedSeconds}s ๊ฒฝ๊ณผ)`);\n }, progressIntervalMs);\n\n child.stdout.on('data', (chunk: Buffer | string) => {\n const text = chunk.toString();\n stdout += text;\n\n if (streamOutput) {\n process.stdout.write(text);\n }\n });\n\n child.stderr.on('data', (chunk: Buffer | string) => {\n const text = chunk.toString();\n stderr += text;\n\n if (streamOutput) {\n process.stderr.write(text);\n }\n });\n\n child.on('error', (error) => {\n clearInterval(progressTimer);\n reject(error);\n });\n\n child.on('close', (code, signal) => {\n clearInterval(progressTimer);\n\n if (code === 0) {\n resolve({\n stderr,\n stdout\n });\n\n return;\n }\n\n const exitSummary = signal ? `signal=${signal}` : `code=${String(code ?? 'unknown')}`;\n reject(new Error(`์‰˜ ๋ช…๋ น ์‹คํ–‰ ์‹คํŒจ (${exitSummary})${stderr.trim() ? `\\n${stderr.trim()}` : ''}`));\n });\n });\n}\n\n/**\n * @description\n * ๋ฆฌ๋ทฐ ๋Œ€์ƒ ํŒŒ์ผ ๋ชฉ๋ก์„ ํ•œ ์ค„ ์š”์•ฝ ๋ฌธ์ž์—ด๋กœ ๋ณ€ํ™˜ํ•ฉ๋‹ˆ๋‹ค.\n * ํŒŒ์ผ์ด ๋งŽ์„ ๋•Œ๋Š” ์ผ๋ถ€๋งŒ ๋…ธ์ถœํ•˜๊ณ  ๋‚˜๋จธ์ง€๋Š” ๊ฐœ์ˆ˜๋กœ ์ถ•์•ฝํ•ด ํ„ฐ๋ฏธ๋„ ์ถœ๋ ฅ์ด ๊ณผ๋„ํ•˜๊ฒŒ ๊ธธ์–ด์ง€์ง€ ์•Š๋„๋ก ํ•ฉ๋‹ˆ๋‹ค.\n * @param files ๋ฆฌ๋ทฐ ๋Œ€์ƒ ํŒŒ์ผ ๋ชฉ๋ก\n * @param visibleCount ํ„ฐ๋ฏธ๋„์— ์ง์ ‘ ๋ณด์—ฌ์ค„ ์ตœ๋Œ€ ํŒŒ์ผ ์ˆ˜\n * @returns ํ™”๋ฉด ํ‘œ์‹œ์šฉ ์š”์•ฝ ๋ฌธ์ž์—ด\n */\nexport function formatReviewTargetFiles(files: string[], visibleCount = 5) {\n if (files.length === 0) {\n return '(์—†์Œ)';\n }\n\n const visibleFiles = files.slice(0, visibleCount);\n const hiddenCount = Math.max(0, files.length - visibleFiles.length);\n\n if (hiddenCount === 0) {\n return visibleFiles.join(', ');\n }\n\n return `${visibleFiles.join(', ')} ์™ธ ${hiddenCount}๊ฐœ`;\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 const timestamp = new Date().toISOString();\n const message = `[${timestamp}][TRACE][${scope}] ${step}${detail ? ` | ${detail}` : ''}`;\n traceMessages.push(message);\n\n if (!enabled) {\n return;\n }\n\n console.log(message);\n };\n}\n\nconst helperTrace = createTraceLogger('helper');\n\nfunction getTimestampParts(now = new Date()) {\n return {\n YYYY: now.getFullYear(),\n MM: String(now.getMonth() + 1).padStart(2, '0'),\n DD: String(now.getDate()).padStart(2, '0'),\n HH: String(now.getHours()).padStart(2, '0'),\n mm: String(now.getMinutes()).padStart(2, '0'),\n ss: String(now.getSeconds()).padStart(2, '0')\n };\n}\n\nfunction getHumanReadableNowString(now = new Date()) {\n const { YYYY, MM, DD, HH, mm, ss } = getTimestampParts(now);\n\n return `${YYYY}-${MM}-${DD} ${HH}:${mm}:${ss}`;\n}\n\nfunction stringifyUnknown(value: unknown) {\n if (value === undefined || value === null) {\n return '';\n }\n\n if (typeof value === 'string') {\n return value;\n }\n\n if (Buffer.isBuffer(value)) {\n return value.toString();\n }\n\n if (value instanceof Error) {\n return value.stack || value.message;\n }\n\n return inspect(value, { depth: 5, breakLength: 120 });\n}\n\nexport function getErrorSummary(error: unknown) {\n if (error instanceof Error) {\n return `${error.name}: ${error.message}`;\n }\n\n return stringifyUnknown(error) || 'Unknown error';\n}\n\nfunction serializeError(error: unknown) {\n const serialized: Record<string, unknown> = {\n summary: getErrorSummary(error)\n };\n\n if (error instanceof Error) {\n serialized.name = error.name;\n serialized.message = error.message;\n serialized.stack = error.stack;\n } else {\n serialized.value = stringifyUnknown(error);\n }\n\n if (error && typeof error === 'object') {\n const errorLike = error as Record<string, unknown>;\n const extraKeys = ['code', 'errno', 'syscall', 'path', 'cmd', 'status', 'signal', 'spawnargs'];\n\n extraKeys.forEach((key) => {\n if (errorLike[key] !== undefined) {\n serialized[key] = errorLike[key];\n }\n });\n\n const stdout = stringifyUnknown(errorLike.stdout);\n if (stdout) {\n serialized.stdout = stdout;\n }\n\n const stderr = stringifyUnknown(errorLike.stderr);\n if (stderr) {\n serialized.stderr = stderr;\n }\n\n const cause = stringifyUnknown(errorLike.cause);\n if (cause) {\n serialized.cause = cause;\n }\n }\n\n return serialized;\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 getAvailableFilePath(dir: string, baseName: string, extension: string) {\n const firstFilePath = path.join(dir, `${baseName}${extension}`);\n if (!fs.existsSync(firstFilePath)) {\n return firstFilePath;\n }\n\n return getNextFilePath(dir, baseName, extension);\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(now = new Date()) {\n const { YYYY, MM, DD, HH, mm, ss } = getTimestampParts(now);\n\n return `${YYYY}-${MM}-${DD}_${HH}-${mm}-${ss}`;\n}\n\nexport function getErrorLogTimestamp(now = new Date()) {\n const { YYYY, MM, DD, HH, mm, ss } = getTimestampParts(now);\n\n return `${YYYY}-${MM}-${DD}-${HH}์‹œ-${mm}๋ถ„-${ss}์ดˆ`;\n}\n\nexport function writeErrorReport(error: unknown, options: WriteErrorReportOptions = {}) {\n try {\n const now = new Date();\n helperTrace('error-report:write:start', options.scope || 'unknown');\n createReportDirectory();\n\n const reportPath = getAvailableFilePath(REPORT_DIR, `error-log-${getErrorLogTimestamp(now)}`, '.md');\n const serializedError = serializeError(error);\n const traceSnapshot = options.traceMessages ?? getTraceMessages();\n const extraSections = options.extraSections || [];\n\n const report = `# Error Log\n\n- ๋ฐœ์ƒ ์‹œ๊ฐ: ${getHumanReadableNowString(now)}\n- Scope: \\`${options.scope || 'unknown'}\\`\n- ์ž‘์—… ๊ฒฝ๋กœ: \\`${process.cwd()}\\`\n- ์‹คํ–‰ ์ธ์ž: \\`${JSON.stringify(options.args ?? process.argv.slice(2))}\\`\n- ์‹คํ–‰ ํ™˜๊ฒฝ: \\`${process.platform} ${process.arch} / Node ${process.version}\\`\n\n## Summary\n\n${options.title || serializedError.summary || 'Unknown error'}\n\n## Error\n\n\\`\\`\\`json\n${JSON.stringify(serializedError, null, 2)}\n\\`\\`\\`\n\n## Trace\n\n\\`\\`\\`json\n${JSON.stringify(traceSnapshot, null, 2)}\n\\`\\`\\`${extraSections.length ? `\\n${extraSections.map((section) => `\\n## ${section.heading}\\n\\n${section.markdown}`).join('\\n')}\\n` : '\\n'}\n`;\n\n fs.writeFileSync(reportPath, report);\n helperTrace('error-report:write:done', reportPath);\n\n return reportPath;\n } catch (writeError) {\n console.error('โš ๏ธ ์—๋Ÿฌ ๋กœ๊ทธ ํŒŒ์ผ ์ƒ์„ฑ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.');\n console.error(writeError);\n\n return '';\n }\n}\n\nexport function exitWithError(\n message: string,\n options: Omit<WriteErrorReportOptions, 'title'> & { error?: unknown } = {}\n): never {\n const reportPath = writeErrorReport(options.error || new Error(message), {\n ...options,\n title: message\n });\n\n console.error(message);\n\n if (options.error) {\n console.error(options.error);\n }\n\n if (reportPath) {\n console.error(`๐Ÿ“„ ์—๋Ÿฌ ๋กœ๊ทธ ์ €์žฅ ์œ„์น˜: ${reportPath}`);\n }\n\n process.exit(1);\n}\n\nfunction parseServiceFromArgs(args: string[] = process.argv.slice(2)): AIServiceType | '' {\n helperTrace('parse-service:start', `args=${JSON.stringify(args)}`);\n const serviceIndex = args.indexOf('--service');\n const rawService = serviceIndex !== -1 ? args[serviceIndex + 1] : '';\n\n if (!rawService) {\n helperTrace('parse-service:empty');\n\n return '';\n }\n\n const normalizedService = rawService.toLowerCase();\n\n if (AIServices.includes(normalizedService as AIServiceType)) {\n helperTrace('parse-service:resolved', normalizedService);\n\n return normalizedService as AIServiceType;\n }\n\n helperTrace('parse-service:invalid', rawService);\n exitWithError(\n `โŒ ์ง€์›ํ•˜์ง€ ์•Š๋Š” ์„œ๋น„์Šค์ž…๋‹ˆ๋‹ค: ${rawService}. ์‚ฌ์šฉ ๊ฐ€๋Šฅ ๊ฐ’: ${AIServices.join(', ')} (์˜ˆ: --service codex)`,\n {\n scope: 'helper:parseServiceFromArgs',\n args,\n extraSections: [\n {\n heading: 'Allowed Services',\n markdown: `\\`\\`\\`json\\n${JSON.stringify(AIServices, null, 2)}\\n\\`\\`\\``\n }\n ]\n }\n );\n}\n\nexport function getGitDiffFilter() {\n const { includePatterns, excludePatterns } = getGitDiffPathspecs();\n const quote = (pattern: string) => `\"${pattern}\"`;\n const includeParams = includePatterns.map(quote).join(' ');\n const excludeParams = excludePatterns.map(quote).join(' ');\n\n return { includeParams, excludeParams };\n}\n\n/**\n * @description\n * ์ปค๋ฐ‹ ์„ ํƒ ๋ชฉ๋ก์—์„œ subject๊ฐ€ ๊ณผํ•˜๊ฒŒ ๊ธธ์–ด์ง€๋Š” ๊ฒƒ์„ ๋ง‰๊ธฐ ์œ„ํ•ด ํ‘œ์‹œ ๊ธธ์ด๋ฅผ ์ œํ•œํ•ฉ๋‹ˆ๋‹ค.\n * commit hash์™€ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ๋Š” ๋ณ„๋„ ํ•„๋“œ๋กœ ์œ ์ง€ํ•˜๋ฏ€๋กœ UI ๊ฐ€๋…์„ฑ์— ํ•„์š”ํ•œ subject๋งŒ ์ž๋ฆ…๋‹ˆ๋‹ค.\n * @param subject ์›๋ณธ ์ปค๋ฐ‹ ์ œ๋ชฉ\n * @returns ํ™”๋ฉด ํ‘œ์‹œ์šฉ ์ œ๋ชฉ\n */\nfunction truncateCommitSubject(subject: string) {\n if (subject.length <= 72) {\n return subject;\n }\n\n return `${subject.slice(0, 69)}...`;\n}\n\n/**\n * @description\n * ์ตœ๊ทผ ์ปค๋ฐ‹ ๋ชฉ๋ก์„ ๋ฆฌ๋ทฐ ์„ ํƒ UI์—์„œ ๋ฐ”๋กœ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋Š” ๊ตฌ์กฐ๋กœ ๋ณ€ํ™˜ํ•ฉ๋‹ˆ๋‹ค.\n * git log ํฌ๋งท์„ ํ•œ ๊ณณ์—์„œ ๊ด€๋ฆฌํ•ด main review์™€ one-by-one review๊ฐ€ ๊ฐ™์€ ์ปค๋ฐ‹ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ๋ฅผ ๊ณต์œ ํ•˜๊ฒŒ ํ•ฉ๋‹ˆ๋‹ค.\n * @returns ์ตœ๊ทผ ์ปค๋ฐ‹ ์˜ต์…˜ ๋ชฉ๋ก\n */\nexport function getRecentCommitOptions(): CommitOption[] {\n const output = runGitCommand(\n ['log', `-${COMMIT_FETCH_LIMIT}`, '--date=relative', '--pretty=format:%h%x09%an%x09%ar%x09%s'],\n { allowFailure: true }\n );\n\n if (!output) {\n return [];\n }\n\n return output.split('\\n').map((line) => {\n const [hash = '', author = '', relativeDate = '', ...subjectParts] = line.split('\\t');\n const subject = subjectParts.join('\\t').trim();\n\n return {\n author,\n description: `${author} | ${relativeDate}`,\n hash,\n label: `${hash} | ${truncateCommitSubject(subject)}`,\n relativeDate,\n subject\n };\n });\n}\n\n/**\n * @description\n * ์„ ํƒ๋œ ์ปค๋ฐ‹ ๋ชฉ๋ก์„ ๋ฆฌํฌํŠธ ๋ฐ diff ํ—ค๋”์— ์žฌ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋Š” markdown bullet ๋ฌธ์ž์—ด๋กœ ์ •๋ฆฌํ•ฉ๋‹ˆ๋‹ค.\n * @param commits ์‚ฌ์šฉ์ž๊ฐ€ ๊ณ ๋ฅธ ์ปค๋ฐ‹ ๋ชฉ๋ก\n * @returns ์ปค๋ฐ‹ ์š”์•ฝ markdown\n */\nexport function buildSelectedCommitSummary(commits: CommitOption[]) {\n return commits.map((commit) => `- ${commit.hash} | ${commit.subject} | ${commit.author} | ${commit.relativeDate}`).join('\\n');\n}\n\n/**\n * @description\n * ๋ฆฌ๋ทฐ ๋Œ€์ƒ pathspec ๋ฐฐ์—ด์„ git argv์— ๋ฐ”๋กœ ๋ถ™์ผ ์ˆ˜ ์žˆ๊ฒŒ ํ‰ํƒ„ํ™”ํ•ฉ๋‹ˆ๋‹ค.\n * ๋ฌธ์ž์—ด ์ปค๋งจ๋“œ์™€ ๋‹ค๋ฅธ ๊ฒฝ๋กœ ๊ธฐ๋ฐ˜ helper๋„ ๊ฐ™์€ ํ•„ํ„ฐ๋ฅผ ๊ณต์œ ํ•˜๋„๋ก helper ํ•จ์ˆ˜๋กœ ๋ถ„๋ฆฌํ•ฉ๋‹ˆ๋‹ค.\n * @returns include/exclude pathspec ๋ฐฐ์—ด\n */\nfunction getReviewPathspecArgs() {\n const { includePatterns, excludePatterns } = getGitDiffPathspecs();\n\n return [...includePatterns, ...excludePatterns];\n}\n\n/**\n * @description\n * ์„ ํƒ๋œ ์—ฌ๋Ÿฌ ์ปค๋ฐ‹์˜ ์ „์ฒด diff๋ฅผ ํ•œ ํŒŒ์ผ์— ํ•ฉ์นฉ๋‹ˆ๋‹ค.\n * ๊ฐ ์ปค๋ฐ‹์„ ๋ณ„๋„ ์„น์…˜์œผ๋กœ ๊ฐ์‹ธ AI๊ฐ€ ์ปค๋ฐ‹ ๊ฒฝ๊ณ„๋ฅผ ์žƒ์ง€ ์•Š๋„๋ก ํ•˜๋ฉฐ, ๊ธฐ์กด ๋ฆฌ๋ทฐ ๋Œ€์ƒ ํ™•์žฅ์ž ํ•„ํ„ฐ๋„ ๊ทธ๋Œ€๋กœ ์ ์šฉํ•ฉ๋‹ˆ๋‹ค.\n * @param commits ์‚ฌ์šฉ์ž๊ฐ€ ๊ณ ๋ฅธ ์ปค๋ฐ‹ ๋ชฉ๋ก\n * @returns ๋ฆฌ๋ทฐ์šฉ ํ†ตํ•ฉ diff ํ…์ŠคํŠธ\n */\nexport function buildSelectedCommitDiff(commits: CommitOption[]) {\n const reviewPathspecArgs = getReviewPathspecArgs();\n const sections = commits\n .map((commit) => {\n const diff = runGitCommand(['show', '--stat', '--patch', '--format=', commit.hash, '--', ...reviewPathspecArgs], {\n allowFailure: true,\n trimOutput: false\n }).trim();\n\n if (!diff) {\n return '';\n }\n\n return [`## ${commit.hash} ${commit.subject}`, diff].join('\\n\\n');\n })\n .filter(Boolean)\n .join('\\n\\n');\n\n if (!sections) {\n return '';\n }\n\n return ['# ์„ ํƒํ•œ ์ปค๋ฐ‹', buildSelectedCommitSummary(commits), '', '# ๋ฆฌ๋ทฐ ๋Œ€์ƒ diff', sections].join('\\n');\n}\n\n/**\n * @description\n * ์„ ํƒ๋œ ์ปค๋ฐ‹์—์„œ ์‹ค์ œ ๋ฆฌ๋ทฐ ๋Œ€์ƒ ํŒŒ์ผ ๋ชฉ๋ก๋งŒ ์ถ”์ถœํ•ฉ๋‹ˆ๋‹ค.\n * one-by-one ๋ฆฌ๋ทฐ๊ฐ€ ํ˜„์žฌ ์„ ํƒ๋œ ์ปค๋ฐ‹ ์ง‘ํ•ฉ์—๋งŒ ๋ฐ˜์‘ํ•˜๋„๋ก commit๋ณ„ name-only ๊ฒฐ๊ณผ๋ฅผ ํ•ฉ์ง‘ํ•ฉ์œผ๋กœ ๊ณ„์‚ฐํ•ฉ๋‹ˆ๋‹ค.\n * @param commits ์‚ฌ์šฉ์ž๊ฐ€ ๊ณ ๋ฅธ ์ปค๋ฐ‹ ๋ชฉ๋ก\n * @returns ์ค‘๋ณต ์ œ๊ฑฐ๋œ ํŒŒ์ผ ๋ชฉ๋ก\n */\nexport function getSelectedCommitFiles(commits: CommitOption[]) {\n const reviewPathspecArgs = getReviewPathspecArgs();\n const files = new Set<string>();\n\n commits.forEach((commit) => {\n const output = runGitCommand(['show', '--pretty=format:', '--name-only', commit.hash, '--', ...reviewPathspecArgs], {\n allowFailure: true\n });\n\n output\n .split('\\n')\n .map((line) => line.trim())\n .filter(Boolean)\n .forEach((filePath) => files.add(filePath));\n });\n\n return [...files];\n}\n\n/**\n * @description\n * ํŠน์ • ํŒŒ์ผ์— ๋Œ€ํ•œ ์„ ํƒ ์ปค๋ฐ‹๋“ค์˜ diff๋งŒ ๋ชจ์•„์„œ one-by-one ๋ฆฌ๋ทฐ ์ž…๋ ฅ์œผ๋กœ ๋งŒ๋“ญ๋‹ˆ๋‹ค.\n * ๋™์ผ ํŒŒ์ผ์ด ์—ฌ๋Ÿฌ ์ปค๋ฐ‹์—์„œ ๋ฐ”๋€ ๊ฒฝ์šฐ commit section์„ ๋‚˜๋ˆ  ๋ณด์—ฌ์ค˜ ํŒŒ์ผ ๋ฆฌ๋ทฐ ์‘๋‹ต์ด ์–ด๋А ๋ณ€๊ฒฝ์„ ๋‹ค๋ฃจ๋Š”์ง€ ์ถ”์  ๊ฐ€๋Šฅํ•˜๊ฒŒ ํ•ฉ๋‹ˆ๋‹ค.\n * @param commits ์‚ฌ์šฉ์ž๊ฐ€ ๊ณ ๋ฅธ ์ปค๋ฐ‹ ๋ชฉ๋ก\n * @param filePath ๋ฆฌ๋ทฐํ•  ํŒŒ์ผ ๊ฒฝ๋กœ\n * @returns ๋‹จ์ผ ํŒŒ์ผ ๋ฆฌ๋ทฐ์šฉ diff ํ…์ŠคํŠธ\n */\nexport function buildSelectedFileDiff(commits: CommitOption[], filePath: string) {\n const sections = commits\n .map((commit) => {\n const diff = runGitCommand(['show', '--stat', '--patch', '--format=', commit.hash, '--', filePath], {\n allowFailure: true,\n trimOutput: false\n }).trim();\n\n if (!diff) {\n return '';\n }\n\n return [`## ${commit.hash} ${commit.subject}`, diff].join('\\n\\n');\n })\n .filter(Boolean)\n .join('\\n\\n');\n\n if (!sections) {\n return '';\n }\n\n return ['# ์„ ํƒํ•œ ์ปค๋ฐ‹', buildSelectedCommitSummary(commits), '', `# ํŒŒ์ผ: ${filePath}`, sections].join('\\n\\n');\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 helperTrace('open-report:start', resolvedPath);\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 helperTrace('open-report:chrome:success', platform);\n console.log('๐Ÿš€ Google Chrome์—์„œ ๋ฆฌํฌํŠธ๋ฅผ ์—ด์—ˆ์Šต๋‹ˆ๋‹ค.');\n\n return;\n }\n } catch (error) {\n helperTrace('open-report:chrome:failed', getErrorSummary(error));\n // Chrome ์‹คํ–‰ ์‹คํŒจ ์‹œ ๊ธฐ๋ณธ ๋ธŒ๋ผ์šฐ์ €๋กœ ํด๋ฐฑ\n }\n\n try {\n if (openWithDefaultBrowser()) {\n helperTrace('open-report:default-browser:success', platform);\n console.log('๐Ÿš€ ๊ธฐ๋ณธ ๋ธŒ๋ผ์šฐ์ €์—์„œ ๋ฆฌํฌํŠธ๋ฅผ ์—ด์—ˆ์Šต๋‹ˆ๋‹ค.');\n\n return;\n }\n } catch (error) {\n helperTrace('open-report:default-browser:failed', getErrorSummary(error));\n console.error('โš ๏ธ ๋ธŒ๋ผ์šฐ์ € ์—ด๊ธฐ ์‹คํŒจ:', error);\n\n return;\n }\n\n helperTrace('open-report:unsupported-platform', platform);\n console.error(`โš ๏ธ ์ง€์›ํ•˜์ง€ ์•Š๋Š” ํ”Œ๋žซํผ์ž…๋‹ˆ๋‹ค: ${platform}`);\n}\n\n/**\n * @description\n * raw mode ๊ธฐ๋ฐ˜ ์„ ํƒ UI๋ฅผ ์•ˆ์ „ํ•˜๊ฒŒ ์—ด ์ˆ˜ ์žˆ๋Š” TTY ํ™˜๊ฒฝ์ธ์ง€ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค.\n * CI๋‚˜ pipe ํ™˜๊ฒฝ์—์„œ๋Š” ์ปค์„œ๋ฅผ ์ œ์–ดํ•  ์ˆ˜ ์—†์œผ๋ฏ€๋กœ ์ฆ‰์‹œ ์—๋Ÿฌ ๋ฆฌํฌํŠธ๋ฅผ ๋‚จ๊ธฐ๊ณ  ์ข…๋ฃŒํ•ฉ๋‹ˆ๋‹ค.\n * @param scope ์—๋Ÿฌ ๋ฆฌํฌํŠธ ๊ตฌ๋ถ„์šฉ scope\n * @param message ์‚ฌ์šฉ์ž์—๊ฒŒ ๋ณด์—ฌ์ค„ ์—๋Ÿฌ ๋ฉ”์‹œ์ง€\n */\nfunction ensureInteractiveSelectionAvailable(scope: string, message: string) {\n if (process.stdin.isTTY && process.stdout.isTTY && typeof process.stdin.setRawMode === 'function') {\n return;\n }\n\n helperTrace(`${scope}:tty-missing`);\n exitWithError(message, {\n scope: `helper:${scope}`\n });\n}\n\n/**\n * @description\n * ์„ ํƒ ๋ชจ๋‹ฌ์„ ํ˜„์žฌ ์œ„์น˜์—์„œ ๋‹ค์‹œ ๊ทธ๋ฆฝ๋‹ˆ๋‹ค.\n * ๊ฐ ์ค„์€ ๋ฏธ๋ฆฌ ํ„ฐ๋ฏธ๋„ ํญ ์•ˆ์œผ๋กœ ์ž˜๋ผ physical line์ด 1์ค„๋กœ ์œ ์ง€๋˜๋ฏ€๋กœ,\n * ์ง์ „ ๋ Œ๋” ์ค„ ์ˆ˜๋งŒํผ ์œ„๋กœ ์ด๋™ํ•œ ๋’ค clearScreenDown ํ•ด๋„ ์ค‘๋ณต ์—†์ด ์•ˆ์ •์ ์œผ๋กœ ๊ฐฑ์‹ ๋ฉ๋‹ˆ๋‹ค.\n * @param lines ์ƒˆ๋กœ ๋ Œ๋”ํ•  ๋ฌธ์ž์—ด ์ค„ ๋ชฉ๋ก\n * @param previousLineCount ์ง์ „ ๋ Œ๋” ์ค„ ์ˆ˜\n * @returns ํ˜„์žฌ ๋ Œ๋” ์ค„ ์ˆ˜\n */\nfunction renderSelectionBlock(lines: string[], previousLineCount: number) {\n if (previousLineCount > 0) {\n readline.moveCursor(process.stdout, 0, -previousLineCount);\n readline.clearScreenDown(process.stdout);\n }\n\n const fittedLines = fitLinesToTerminal(lines);\n process.stdout.write(`${fittedLines.join('\\n')}\\n`);\n\n return fittedLines.length;\n}\n\n/**\n * @description\n * ํ˜„์žฌ ์ปค์„œ ๊ธฐ์ค€ visible window๋ฅผ ๊ณ„์‚ฐํ•ด ๊ธด ์ปค๋ฐ‹ ๋ชฉ๋ก๋„ ๊ณ ์ • ๋†’์ด๋กœ ํƒ์ƒ‰ํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋งŒ๋“ญ๋‹ˆ๋‹ค.\n * @param optionCount ์ „์ฒด ์˜ต์…˜ ๊ฐœ์ˆ˜\n * @param selectedIndex ํ˜„์žฌ ํฌ์ปค์Šค ์ธ๋ฑ์Šค\n * @param windowSize ๋™์‹œ์— ๋ณด์—ฌ์ค„ ์ตœ๋Œ€ ์˜ต์…˜ ๊ฐœ์ˆ˜\n * @returns ์‹œ์ž‘/๋ ์ธ๋ฑ์Šค\n */\nfunction getSelectionWindowRange(optionCount: number, selectedIndex: number, windowSize: number) {\n if (optionCount <= windowSize) {\n return {\n end: optionCount,\n start: 0\n };\n }\n\n const halfWindow = Math.floor(windowSize / 2);\n const maxStart = optionCount - windowSize;\n const start = Math.max(0, Math.min(selectedIndex - halfWindow, maxStart));\n\n return {\n end: Math.min(optionCount, start + windowSize),\n start\n };\n}\n\n/**\n * @description\n * ๊ณตํ†ต multi-select UI ๋ Œ๋”๋ง์— ํ•„์š”ํ•œ ๋ฌธ์ž์—ด ๋ชฉ๋ก์„ ๊ตฌ์„ฑํ•ฉ๋‹ˆ๋‹ค.\n * ์„ค๋ช… ํ…์ŠคํŠธ๋Š” dim ์ฒ˜๋ฆฌํ•ด subject์™€ author/date๊ฐ€ ํ•œ ์ค„์—์„œ ๊ตฌ๋ถ„๋˜๋„๋ก ํ•ฉ๋‹ˆ๋‹ค.\n * @param question ์งˆ๋ฌธ ํ—ค๋”\n * @param options ํ‘œ์‹œํ•  ์˜ต์…˜ ๋ชฉ๋ก\n * @param selectedIndex ํ˜„์žฌ ํฌ์ปค์Šค ์ธ๋ฑ์Šค\n * @param toggled ์„ ํƒ๋œ ์ธ๋ฑ์Šค Set\n * @param windowSize ๋™์‹œ์— ๋ณด์—ฌ์ค„ ์ตœ๋Œ€ ์˜ต์…˜ ๊ฐœ์ˆ˜\n * @returns ์ถœ๋ ฅ ์ค„ ๋ชฉ๋ก\n */\nfunction buildMultiSelectLines<T>(\n question: string,\n options: MultiSelectOption<T>[],\n selectedIndex: number,\n toggled: Set<number>,\n windowSize: number\n) {\n const { start, end } = getSelectionWindowRange(options.length, selectedIndex, windowSize);\n const lines = [\n `${ANSI.bold}${question}${ANSI.reset}`,\n `${ANSI.dim}โ†‘โ†“ ์ด๋™ | Space ์„ ํƒ/ํ•ด์ œ | Enter ์™„๋ฃŒ | Esc ์ทจ์†Œ${ANSI.reset}`,\n `${ANSI.dim}์„ ํƒ๋จ: ${toggled.size}๊ฐœ / ์ „์ฒด: ${options.length}๊ฐœ${ANSI.reset}`\n ];\n\n for (let index = start; index < end; index += 1) {\n const option = options[index];\n\n if (!option) {\n continue;\n }\n\n const cursor = index === selectedIndex ? `${ANSI.cyan}>${ANSI.reset}` : ' ';\n const checked = toggled.has(index) ? `${ANSI.green}โ˜‘${ANSI.reset}` : 'โ˜';\n const description = option.description ? ` ${ANSI.dim}${option.description}${ANSI.reset}` : '';\n lines.push(`${cursor} ${checked} ${option.label}${description}`);\n }\n\n if (options.length > windowSize) {\n lines.push(`${ANSI.dim}ํ‘œ์‹œ ๋ฒ”์œ„: ${start + 1}-${end} / ${options.length}${ANSI.reset}`);\n }\n\n return lines;\n}\n\n/**\n * @description\n * ์ปค๋ฐ‹ ์„ ํƒ์ฒ˜๋Ÿผ ๋ณต์ˆ˜ ์„ ํƒ์ด ํ•„์š”ํ•œ ํ„ฐ๋ฏธ๋„ ๋ชจ๋‹ฌ์„ ๊ณตํ†ต์œผ๋กœ ์ฒ˜๋ฆฌํ•ฉ๋‹ˆ๋‹ค.\n * raw mode๋ฅผ ์ง์ ‘ ์ œ์–ดํ•ด ๋ฐฉํ–ฅํ‚ค, Space, Enter, Esc๋ฅผ ์ฝ๊ณ  ์„ ํƒ๋œ value ๋ชฉ๋ก๋งŒ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.\n * @param question ์‚ฌ์šฉ์ž ์•ˆ๋‚ด ๋ฌธ๊ตฌ\n * @param options ์„ ํƒ ์˜ต์…˜ ๋ชฉ๋ก\n * @param windowSize ๋™์‹œ์— ๋ณด์—ฌ์ค„ ์ตœ๋Œ€ ์˜ต์…˜ ๊ฐœ์ˆ˜\n * @returns ์‚ฌ์šฉ์ž๊ฐ€ ํ™•์ •ํ•œ ์„ ํƒ ๊ฐ’ ๋ฐฐ์—ด\n */\nexport async function showMultiSelect<T>(question: string, options: MultiSelectOption<T>[], windowSize = COMMIT_SELECTION_WINDOW) {\n ensureInteractiveSelectionAvailable('showMultiSelect', 'โŒ ์ปค๋ฐ‹ ์„ ํƒ ๋ชจ๋‹ฌ์€ TTY ํ™˜๊ฒฝ์—์„œ๋งŒ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.');\n let selectedIndex = 0;\n let renderedLineCount = 0;\n const toggled = new Set<number>();\n const rl = readline.createInterface({\n input: process.stdin,\n output: process.stdout,\n terminal: true\n });\n\n process.stdout.write('\\u001b[?25l');\n\n const cleanup = () => {\n if (renderedLineCount > 0) {\n readline.moveCursor(process.stdout, 0, -renderedLineCount);\n readline.clearScreenDown(process.stdout);\n renderedLineCount = 0;\n }\n process.stdin.removeListener('data', onData);\n process.stdin.setRawMode(false);\n process.stdin.pause();\n rl.close();\n process.stdout.write('\\u001b[?25h');\n };\n\n const render = () => {\n const lines = buildMultiSelectLines(question, options, selectedIndex, toggled, windowSize);\n renderedLineCount = renderSelectionBlock(lines, renderedLineCount);\n };\n\n const confirmSelection = (resolve: (value: T[]) => void) => {\n const values = [...toggled]\n .sort((left, right) => left - right)\n .map((index) => options[index]?.value)\n .filter((value): value is T => value !== undefined);\n\n cleanup();\n resolve(values);\n };\n\n const cancelSelection = (resolve: (value: T[]) => void) => {\n cleanup();\n resolve([]);\n };\n\n let onData = (_data: Buffer) => {\n // ์‹ค์ œ ๊ตฌํ˜„์€ Promise ์ƒ์„ฑ ์‹œ์ ์— ๋ฐ”์ธ๋”ฉํ•ฉ๋‹ˆ๋‹ค.\n };\n\n render();\n\n return new Promise<T[]>((resolve) => {\n onData = (data: Buffer) => {\n const key = data.toString();\n\n if (key === '\\u0003') {\n cleanup();\n process.exit(0);\n }\n\n if (key === '\\u001b') {\n cancelSelection(resolve);\n\n return;\n }\n\n if (key === '\\x1b[A') {\n selectedIndex = (selectedIndex - 1 + options.length) % options.length;\n render();\n\n return;\n }\n\n if (key === '\\x1b[B') {\n selectedIndex = (selectedIndex + 1) % options.length;\n render();\n\n return;\n }\n\n if (key === ' ') {\n if (toggled.has(selectedIndex)) {\n toggled.delete(selectedIndex);\n } else {\n toggled.add(selectedIndex);\n }\n render();\n\n return;\n }\n\n if (key === '\\r' || key === '\\n') {\n confirmSelection(resolve);\n }\n };\n\n process.stdin.setRawMode(true);\n process.stdin.resume();\n process.stdin.on('data', onData);\n });\n}\n\n/**\n * @description\n * ์ตœ๊ทผ ์ปค๋ฐ‹ ๋ชฉ๋ก์„ modal UI๋กœ ์„ ํƒ๋ฐ›์•„ review ์—”ํŠธ๋ฆฌํฌ์ธํŠธ์—์„œ ๋ฐ”๋กœ ์žฌ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.\n * @returns ์‚ฌ์šฉ์ž๊ฐ€ ๊ณ ๋ฅธ ์ปค๋ฐ‹ ๋ชฉ๋ก\n */\nexport async function selectReviewCommits() {\n const commits = getRecentCommitOptions();\n\n if (commits.length === 0) {\n console.log('โ„น๏ธ ๋ฆฌ๋ทฐํ•  ์ตœ๊ทผ ์ปค๋ฐ‹์ด ์—†์Šต๋‹ˆ๋‹ค.');\n\n return [];\n }\n\n return showMultiSelect<CommitOption>(\n '๋ฆฌ๋ทฐํ•  ์ปค๋ฐ‹์„ ์„ ํƒํ•ด์ฃผ์„ธ์š”.',\n commits.map((commit) => ({\n description: commit.description,\n label: commit.label,\n value: commit\n })),\n COMMIT_SELECTION_WINDOW\n );\n}\n\n/**\n * AI ์„œ๋น„์Šค ์„ ํƒ\n */\nexport function selectAIService() {\n const service = parseServiceFromArgs();\n\n if (!service) {\n helperTrace('select-service:missing');\n exitWithError('โŒ ์„œ๋น„์Šค๊ฐ€ ์„ ํƒ๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค.', {\n scope: 'helper:selectAIService'\n });\n }\n\n helperTrace('select-service:done', service);\n\n return service;\n}\n\n/**\n * ํ„ฐ๋ฏธ๋„์—์„œ ๋ผ๋””์˜ค ๋ฒ„ํŠผ ํ˜•ํƒœ๋กœ AI ์„œ๋น„์Šค๋ฅผ ์„ ํƒํ•ฉ๋‹ˆ๋‹ค.\n */\nexport async function showSelectionAIService(): Promise<AIServiceType> {\n const selectedServiceFromArgs = parseServiceFromArgs();\n\n if (selectedServiceFromArgs) {\n helperTrace('show-selection:from-args', selectedServiceFromArgs);\n console.log(`\\nโœ… ${ANSI.green}${selectedServiceFromArgs}${ANSI.reset} ์„œ๋น„์Šค๊ฐ€ ์„ ํƒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. (--service)\\n`);\n\n return selectedServiceFromArgs;\n }\n\n ensureInteractiveSelectionAvailable('showSelectionAIService', 'โŒ AI ์„œ๋น„์Šค ์„ ํƒ UI๋Š” TTY ํ™˜๊ฒฝ์—์„œ๋งŒ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.');\n helperTrace('show-selection:interactive:start');\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 helperTrace('show-selection:interactive:render', AIServices[selectedIndex] || 'unknown');\n\n // Clear everything from cursor down to avoid ghosting/overlaps\n // ์ž”์ƒ์ด๋‚˜ ๊ฒน์นจ์„ ๋ฐฉ์ง€ํ•˜๊ธฐ ์œ„ํ•ด ์ปค์„œ ์œ„์น˜๋ถ€ํ„ฐ ์•„๋ž˜์ชฝ ๋ชจ๋‘ ์ง€์›€\n readline.clearScreenDown(process.stdout);\n\n process.stdout.write(\n `๐Ÿค– AI ์„œ๋น„์Šค๋ฅผ ์„ ํƒํ•ด์ฃผ์„ธ์š” (${ANSI.yellow}โ†‘โ†“ ๋ฐฉํ–ฅํ‚ค${ANSI.reset} ์ด๋™, ${ANSI.yellow}Enter${ANSI.reset} ์„ ํƒ):\\n`\n );\n AIServices.forEach((service, index) => {\n if (index === selectedIndex) {\n process.stdout.write(` ${ANSI.cyan}>${ANSI.reset} ${ANSI.cyan}โ—‰${ANSI.reset} ${ANSI.bold}${service}${ANSI.reset}\\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 helperTrace('show-selection:interactive: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โœ… ${ANSI.green}${AIServices[selectedIndex]}${ANSI.reset} ์„œ๋น„์Šค๊ฐ€ ์„ ํƒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.\\n`);\n const result = AIServices[selectedIndex];\n if (result) {\n helperTrace('show-selection:interactive:confirmed', 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, exitWithError, getErrorSummary } from '../../common/helper';\n\nconst trace = createTraceLogger('installation-gemini');\n\n// gemini-cli ์„ค์น˜ ํ™•์ธ ๋ฐ ์„ค์น˜\nexport function checkGeminiCliInstalled() {\n trace('checkGeminiCliInstalled:start');\n try {\n trace('version-check:run', 'gemini --version');\n execSync('gemini --version', { stdio: 'ignore' });\n trace('version-check:ok');\n } catch (error) {\n trace('version-check:failed', getErrorSummary(error));\n trace('install:start', '@google/gemini-cli');\n console.log('โ„น๏ธ gemini-cli๊ฐ€ ์„ค์น˜๋˜์–ด ์žˆ์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ์„ค์น˜๋ฅผ ์ง„ํ–‰ํ•ฉ๋‹ˆ๋‹ค... npm install -g @google/gemini-cli');\n try {\n execSync('npm install -g @google/gemini-cli', { stdio: 'inherit' });\n trace('install:ok', 'login-required');\n console.log('โœ… gemini-cli ์„ค์น˜๊ฐ€ ์™„๋ฃŒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.');\n console.log('โš ๏ธ Gemini API ์‚ฌ์šฉ์„ ์œ„ํ•ด ์ธ์ฆ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.');\n console.log(' ํ„ฐ๋ฏธ๋„์—์„œ \"gemini\" ๋ฅผ ์ž…๋ ฅํ•˜์—ฌ ๋ธŒ๋ผ์šฐ์ € ๋กœ๊ทธ์ธ์„ ์™„๋ฃŒํ•œ ํ›„, ๋‹ค์‹œ ์‹œ๋„ํ•ด์ฃผ์„ธ์š”.');\n process.exit(1);\n } catch (installError) {\n trace('install:failed', getErrorSummary(installError));\n exitWithError('โŒ gemini-cli ์„ค์น˜ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค. ๊ถŒํ•œ ๋ฌธ์ œ์ผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค (sudo ํ•„์š”).', {\n scope: 'installation-gemini',\n error: installError\n });\n }\n }\n trace('checkGeminiCliInstalled:end');\n}\n"]}