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,19 +1,59 @@
1
1
  #!/usr/bin/env node
2
- import { execSync } from 'child_process';
3
2
  import fs from 'fs';
3
+ import { execSync, execFileSync } from 'child_process';
4
4
  import path from 'path';
5
5
  import readline from 'readline';
6
6
  import { fileURLToPath } from 'url';
7
+ import { inspect } from 'util';
7
8
 
8
9
  var __dirname = path.dirname(fileURLToPath(import.meta.url));
9
- var rulesPath = path.resolve(__dirname, "../../src/common/rules/review-rules.md");
10
- var namingRulesPath = path.resolve(__dirname, "../../src/common/rules/naming-rule.md");
11
- var codingConventionRulesPath = path.resolve(__dirname, "../../src/common/rules/coding-convention.md");
12
- var reviewFormPath = path.resolve(__dirname, "../../src/common/form/review-form.md");
13
- path.resolve(__dirname, "../../src/common/form/review-form-one-by-one.md");
10
+ var traceMessages = [];
11
+ var GEMINI_CLI_PACKAGE_NAME = "sales-frontend-gemini-cli";
12
+ var cachedPackageRootPath = "";
13
+ function isGeminiCliPackageRoot(directory) {
14
+ const packageJsonPath = path.join(directory, "package.json");
15
+ if (!fs.existsSync(packageJsonPath)) {
16
+ return false;
17
+ }
18
+ try {
19
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
20
+ return packageJson.name === GEMINI_CLI_PACKAGE_NAME;
21
+ } catch {
22
+ return false;
23
+ }
24
+ }
25
+ function resolveGeminiCliPackageRoot(startDirectory = __dirname) {
26
+ if (cachedPackageRootPath) {
27
+ return cachedPackageRootPath;
28
+ }
29
+ let currentDirectory = startDirectory;
30
+ while (true) {
31
+ if (isGeminiCliPackageRoot(currentDirectory)) {
32
+ cachedPackageRootPath = currentDirectory;
33
+ return cachedPackageRootPath;
34
+ }
35
+ const parentDirectory = path.dirname(currentDirectory);
36
+ if (parentDirectory === currentDirectory) {
37
+ break;
38
+ }
39
+ currentDirectory = parentDirectory;
40
+ }
41
+ cachedPackageRootPath = path.resolve(startDirectory, "../..");
42
+ return cachedPackageRootPath;
43
+ }
44
+ function resolvePackageAssetPath(relativeFilePath) {
45
+ return path.resolve(resolveGeminiCliPackageRoot(), relativeFilePath);
46
+ }
47
+ var rulesPath = resolvePackageAssetPath("src/common/rules/review-rules.md");
48
+ var namingRulesPath = resolvePackageAssetPath("src/common/rules/naming-rule.md");
49
+ var codingConventionRulesPath = resolvePackageAssetPath("src/common/rules/coding-convention.md");
50
+ var reviewFormPath = resolvePackageAssetPath("src/common/form/review-form.md");
51
+ resolvePackageAssetPath("src/common/form/review-form-one-by-one.md");
14
52
  var REPORT_DIR = ".review-report";
15
53
  var tempDiffPath = "temp_diff.txt";
16
54
  var AIServices = ["gemini", "claude", "codex"];
55
+ var COMMIT_FETCH_LIMIT = 20;
56
+ var COMMIT_SELECTION_WINDOW = 8;
17
57
  var ignoreList = [
18
58
  "package.json",
19
59
  "*.yml",
@@ -28,33 +68,276 @@ var ignoreList = [
28
68
  ".review-report/"
29
69
  // 생성되는 리포트 폴더도 제외
30
70
  ];
31
- function parseServiceFromArgs(args4 = process.argv.slice(2)) {
32
- const serviceIndex = args4.indexOf("--service");
33
- const rawService = serviceIndex !== -1 ? args4[serviceIndex + 1] : "";
34
- if (!rawService) {
71
+ function isTestMode(args4 = process.argv.slice(2)) {
72
+ return args4.includes("--test");
73
+ }
74
+ function clearTraceMessages() {
75
+ traceMessages.length = 0;
76
+ }
77
+ function getTraceMessages() {
78
+ return [...traceMessages];
79
+ }
80
+ var ANSI = {
81
+ bold: "\x1B[1m",
82
+ cyan: "\x1B[36m",
83
+ dim: "\x1B[2m",
84
+ green: "\x1B[32m",
85
+ reset: "\x1B[0m",
86
+ yellow: "\x1B[33m"
87
+ };
88
+ var ANSI_PATTERN = new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`, "g");
89
+ var COMBINING_MARK_PATTERN = /\p{Mark}/u;
90
+ var GRAPHEME_SEGMENTER = typeof Intl !== "undefined" && "Segmenter" in Intl ? new Intl.Segmenter("ko", { granularity: "grapheme" }) : null;
91
+ function getGitDiffPathspecs() {
92
+ return {
93
+ excludePatterns: ignoreList.map((item) => `:(exclude)${item}`),
94
+ includePatterns: ["*.ts", "*.tsx", "*.js", "*.jsx"]
95
+ };
96
+ }
97
+ function segmentGraphemes(value) {
98
+ if (!GRAPHEME_SEGMENTER) {
99
+ return [...value];
100
+ }
101
+ return [...GRAPHEME_SEGMENTER.segment(value)].map(({ segment }) => segment);
102
+ }
103
+ function isWideCodePoint(codePoint) {
104
+ return codePoint >= 4352 && (codePoint <= 4447 || codePoint === 9001 || codePoint === 9002 || codePoint >= 11904 && codePoint <= 12871 && codePoint !== 12351 || codePoint >= 12880 && codePoint <= 19903 || codePoint >= 19968 && codePoint <= 42182 || codePoint >= 43360 && codePoint <= 43388 || codePoint >= 44032 && codePoint <= 55203 || codePoint >= 63744 && codePoint <= 64255 || codePoint >= 65040 && codePoint <= 65049 || codePoint >= 65072 && codePoint <= 65131 || codePoint >= 65281 && codePoint <= 65376 || codePoint >= 65504 && codePoint <= 65510 || codePoint >= 127488 && codePoint <= 127569 || codePoint >= 131072 && codePoint <= 262141);
105
+ }
106
+ function isEmojiCodePoint(codePoint) {
107
+ return codePoint >= 127462 && codePoint <= 127487 || codePoint >= 127744 && codePoint <= 129791 || codePoint >= 9728 && codePoint <= 10175;
108
+ }
109
+ function getGraphemeWidth(grapheme) {
110
+ let width = 0;
111
+ for (const character of grapheme) {
112
+ const codePoint = character.codePointAt(0);
113
+ if (!codePoint || COMBINING_MARK_PATTERN.test(character) || codePoint === 8205) {
114
+ continue;
115
+ }
116
+ if (codePoint >= 65024 && codePoint <= 65039 || codePoint >= 917760 && codePoint <= 917999) {
117
+ continue;
118
+ }
119
+ if (isWideCodePoint(codePoint) || isEmojiCodePoint(codePoint)) {
120
+ width = Math.max(width, 2);
121
+ continue;
122
+ }
123
+ width = Math.max(width, 1);
124
+ }
125
+ return width;
126
+ }
127
+ function tokenizePlainText(value) {
128
+ return segmentGraphemes(value).map((segment) => ({
129
+ value: segment,
130
+ visibleWidth: getGraphemeWidth(segment)
131
+ }));
132
+ }
133
+ function tokenizeVisibleText(value) {
134
+ const tokens = [];
135
+ let lastIndex = 0;
136
+ for (const match of value.matchAll(ANSI_PATTERN)) {
137
+ const index = match.index ?? 0;
138
+ if (index > lastIndex) {
139
+ tokens.push(...tokenizePlainText(value.slice(lastIndex, index)));
140
+ }
141
+ tokens.push({
142
+ value: match[0],
143
+ visibleWidth: 0
144
+ });
145
+ lastIndex = index + match[0].length;
146
+ }
147
+ if (lastIndex < value.length) {
148
+ tokens.push(...tokenizePlainText(value.slice(lastIndex)));
149
+ }
150
+ return tokens;
151
+ }
152
+ function truncateLineForTerminal(value, maxWidth) {
153
+ if (maxWidth <= 0) {
35
154
  return "";
36
155
  }
37
- const normalizedService = rawService.toLowerCase();
38
- if (AIServices.includes(normalizedService)) {
39
- return normalizedService;
156
+ const tokens = tokenizeVisibleText(value);
157
+ const totalWidth = tokens.reduce((sum, token) => sum + token.visibleWidth, 0);
158
+ if (totalWidth <= maxWidth) {
159
+ return value;
40
160
  }
41
- console.error(
42
- `\u274C \uC9C0\uC6D0\uD558\uC9C0 \uC54A\uB294 \uC11C\uBE44\uC2A4\uC785\uB2C8\uB2E4: ${rawService}. \uC0AC\uC6A9 \uAC00\uB2A5 \uAC12: ${AIServices.join(", ")} (\uC608: --service codex)`
43
- );
44
- process.exit(1);
161
+ const ellipsis = "...";
162
+ const ellipsisWidth = 3;
163
+ const targetWidth = Math.max(0, maxWidth - ellipsisWidth);
164
+ let usedWidth = 0;
165
+ let result = "";
166
+ for (const token of tokens) {
167
+ if (token.visibleWidth === 0) {
168
+ result += token.value;
169
+ continue;
170
+ }
171
+ if (usedWidth + token.visibleWidth > targetWidth) {
172
+ break;
173
+ }
174
+ result += token.value;
175
+ usedWidth += token.visibleWidth;
176
+ }
177
+ return `${result}${ellipsis}${ANSI.reset}`;
45
178
  }
46
- function isTestMode(args4 = process.argv.slice(2)) {
47
- return args4.includes("--test");
179
+ function fitLinesToTerminal(lines) {
180
+ const maxWidth = Math.max(20, (process.stdout.columns || 120) - 1);
181
+ return lines.map((line) => truncateLineForTerminal(line, maxWidth));
182
+ }
183
+ function runGitCommand(args4, options = {}) {
184
+ const { allowFailure = false, trimOutput = true } = options;
185
+ try {
186
+ const output = execFileSync("git", args4, {
187
+ encoding: "utf8",
188
+ maxBuffer: 1024 * 1024 * 20,
189
+ stdio: ["ignore", "pipe", "pipe"]
190
+ });
191
+ return trimOutput ? output.trim() : output;
192
+ } catch (error) {
193
+ helperTrace("git-command:failed", `${args4.join(" ")} | ${getErrorSummary(error)}`);
194
+ if (allowFailure) {
195
+ return "";
196
+ }
197
+ throw error;
198
+ }
199
+ }
200
+ async function executeShellCommandWithProgress(command, options = {}) {
201
+ const { progressIntervalMs = 1e4, progressMessage = "\u23F3 \uBA85\uB839\uC744 \uC2E4\uD589\uD558\uB294 \uC911\uC785\uB2C8\uB2E4...", streamOutput = false } = options;
202
+ const { spawn } = await import('child_process');
203
+ return new Promise((resolve, reject) => {
204
+ let stdout = "";
205
+ let stderr = "";
206
+ const startedAt = Date.now();
207
+ console.log(progressMessage);
208
+ const child = spawn("/bin/zsh", ["-lc", command], {
209
+ stdio: ["ignore", "pipe", "pipe"]
210
+ });
211
+ const progressTimer = setInterval(() => {
212
+ const elapsedSeconds = Math.max(1, Math.floor((Date.now() - startedAt) / 1e3));
213
+ console.log(`${progressMessage} (${elapsedSeconds}s \uACBD\uACFC)`);
214
+ }, progressIntervalMs);
215
+ child.stdout.on("data", (chunk) => {
216
+ const text = chunk.toString();
217
+ stdout += text;
218
+ if (streamOutput) {
219
+ process.stdout.write(text);
220
+ }
221
+ });
222
+ child.stderr.on("data", (chunk) => {
223
+ const text = chunk.toString();
224
+ stderr += text;
225
+ if (streamOutput) {
226
+ process.stderr.write(text);
227
+ }
228
+ });
229
+ child.on("error", (error) => {
230
+ clearInterval(progressTimer);
231
+ reject(error);
232
+ });
233
+ child.on("close", (code, signal) => {
234
+ clearInterval(progressTimer);
235
+ if (code === 0) {
236
+ resolve({
237
+ stderr,
238
+ stdout
239
+ });
240
+ return;
241
+ }
242
+ const exitSummary = signal ? `signal=${signal}` : `code=${String(code ?? "unknown")}`;
243
+ reject(new Error(`\uC258 \uBA85\uB839 \uC2E4\uD589 \uC2E4\uD328 (${exitSummary})${stderr.trim() ? `
244
+ ${stderr.trim()}` : ""}`));
245
+ });
246
+ });
247
+ }
248
+ function formatReviewTargetFiles(files, visibleCount = 5) {
249
+ if (files.length === 0) {
250
+ return "(\uC5C6\uC74C)";
251
+ }
252
+ const visibleFiles = files.slice(0, visibleCount);
253
+ const hiddenCount = Math.max(0, files.length - visibleFiles.length);
254
+ if (hiddenCount === 0) {
255
+ return visibleFiles.join(", ");
256
+ }
257
+ return `${visibleFiles.join(", ")} \uC678 ${hiddenCount}\uAC1C`;
48
258
  }
49
259
  function createTraceLogger(scope, args4 = process.argv.slice(2)) {
50
260
  const enabled = isTestMode(args4);
51
261
  return (step, detail) => {
262
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
263
+ const message = `[${timestamp}][TRACE][${scope}] ${step}${detail ? ` | ${detail}` : ""}`;
264
+ traceMessages.push(message);
52
265
  if (!enabled) {
53
266
  return;
54
267
  }
55
- console.log(`[TRACE][${scope}] ${step}${detail ? ` | ${detail}` : ""}`);
268
+ console.log(message);
269
+ };
270
+ }
271
+ var helperTrace = createTraceLogger("helper");
272
+ function getTimestampParts(now = /* @__PURE__ */ new Date()) {
273
+ return {
274
+ YYYY: now.getFullYear(),
275
+ MM: String(now.getMonth() + 1).padStart(2, "0"),
276
+ DD: String(now.getDate()).padStart(2, "0"),
277
+ HH: String(now.getHours()).padStart(2, "0"),
278
+ mm: String(now.getMinutes()).padStart(2, "0"),
279
+ ss: String(now.getSeconds()).padStart(2, "0")
56
280
  };
57
281
  }
282
+ function getHumanReadableNowString(now = /* @__PURE__ */ new Date()) {
283
+ const { YYYY, MM, DD, HH, mm, ss } = getTimestampParts(now);
284
+ return `${YYYY}-${MM}-${DD} ${HH}:${mm}:${ss}`;
285
+ }
286
+ function stringifyUnknown(value) {
287
+ if (value === void 0 || value === null) {
288
+ return "";
289
+ }
290
+ if (typeof value === "string") {
291
+ return value;
292
+ }
293
+ if (Buffer.isBuffer(value)) {
294
+ return value.toString();
295
+ }
296
+ if (value instanceof Error) {
297
+ return value.stack || value.message;
298
+ }
299
+ return inspect(value, { depth: 5, breakLength: 120 });
300
+ }
301
+ function getErrorSummary(error) {
302
+ if (error instanceof Error) {
303
+ return `${error.name}: ${error.message}`;
304
+ }
305
+ return stringifyUnknown(error) || "Unknown error";
306
+ }
307
+ function serializeError(error) {
308
+ const serialized = {
309
+ summary: getErrorSummary(error)
310
+ };
311
+ if (error instanceof Error) {
312
+ serialized.name = error.name;
313
+ serialized.message = error.message;
314
+ serialized.stack = error.stack;
315
+ } else {
316
+ serialized.value = stringifyUnknown(error);
317
+ }
318
+ if (error && typeof error === "object") {
319
+ const errorLike = error;
320
+ const extraKeys = ["code", "errno", "syscall", "path", "cmd", "status", "signal", "spawnargs"];
321
+ extraKeys.forEach((key) => {
322
+ if (errorLike[key] !== void 0) {
323
+ serialized[key] = errorLike[key];
324
+ }
325
+ });
326
+ const stdout = stringifyUnknown(errorLike.stdout);
327
+ if (stdout) {
328
+ serialized.stdout = stdout;
329
+ }
330
+ const stderr = stringifyUnknown(errorLike.stderr);
331
+ if (stderr) {
332
+ serialized.stderr = stderr;
333
+ }
334
+ const cause = stringifyUnknown(errorLike.cause);
335
+ if (cause) {
336
+ serialized.cause = cause;
337
+ }
338
+ }
339
+ return serialized;
340
+ }
58
341
  function getNextFilePath(dir, baseName, extension) {
59
342
  let counter = 1;
60
343
  while (true) {
@@ -65,6 +348,13 @@ function getNextFilePath(dir, baseName, extension) {
65
348
  counter++;
66
349
  }
67
350
  }
351
+ function getAvailableFilePath(dir, baseName, extension) {
352
+ const firstFilePath = path.join(dir, `${baseName}${extension}`);
353
+ if (!fs.existsSync(firstFilePath)) {
354
+ return firstFilePath;
355
+ }
356
+ return getNextFilePath(dir, baseName, extension);
357
+ }
68
358
  function deleteFile(filePath) {
69
359
  if (fs.existsSync(filePath)) {
70
360
  fs.unlinkSync(filePath);
@@ -78,27 +368,171 @@ function createReportDirectory() {
78
368
  fs.mkdirSync(REPORT_DIR, { recursive: true });
79
369
  }
80
370
  }
81
- function getNowString() {
82
- const now = /* @__PURE__ */ new Date();
83
- const YYYY = now.getFullYear();
84
- const MM = String(now.getMonth() + 1).padStart(2, "0");
85
- const DD = String(now.getDate()).padStart(2, "0");
86
- const HH = String(now.getHours()).padStart(2, "0");
87
- const mm = String(now.getMinutes()).padStart(2, "0");
88
- const ss = String(now.getSeconds()).padStart(2, "0");
371
+ function getNowString(now = /* @__PURE__ */ new Date()) {
372
+ const { YYYY, MM, DD, HH, mm, ss } = getTimestampParts(now);
89
373
  return `${YYYY}-${MM}-${DD}_${HH}-${mm}-${ss}`;
90
374
  }
91
- function getGitDiffFilter() {
92
- const includeExtensions = ["*.ts", "*.tsx", "*.js", "*.jsx"];
93
- const excludePatterns = ignoreList.map((item) => `:(exclude)${item}`);
94
- const quote = (pattern) => `"${pattern}"`;
95
- const includeParams = includeExtensions.map(quote).join(" ");
96
- const excludeParams = excludePatterns.map(quote).join(" ");
97
- return { includeParams, excludeParams };
375
+ function getErrorLogTimestamp(now = /* @__PURE__ */ new Date()) {
376
+ const { YYYY, MM, DD, HH, mm, ss } = getTimestampParts(now);
377
+ return `${YYYY}-${MM}-${DD}-${HH}\uC2DC-${mm}\uBD84-${ss}\uCD08`;
378
+ }
379
+ function writeErrorReport(error, options = {}) {
380
+ try {
381
+ const now = /* @__PURE__ */ new Date();
382
+ helperTrace("error-report:write:start", options.scope || "unknown");
383
+ createReportDirectory();
384
+ const reportPath = getAvailableFilePath(REPORT_DIR, `error-log-${getErrorLogTimestamp(now)}`, ".md");
385
+ const serializedError = serializeError(error);
386
+ const traceSnapshot = options.traceMessages ?? getTraceMessages();
387
+ const extraSections = options.extraSections || [];
388
+ const report = `# Error Log
389
+
390
+ - \uBC1C\uC0DD \uC2DC\uAC01: ${getHumanReadableNowString(now)}
391
+ - Scope: \`${options.scope || "unknown"}\`
392
+ - \uC791\uC5C5 \uACBD\uB85C: \`${process.cwd()}\`
393
+ - \uC2E4\uD589 \uC778\uC790: \`${JSON.stringify(options.args ?? process.argv.slice(2))}\`
394
+ - \uC2E4\uD589 \uD658\uACBD: \`${process.platform} ${process.arch} / Node ${process.version}\`
395
+
396
+ ## Summary
397
+
398
+ ${options.title || serializedError.summary || "Unknown error"}
399
+
400
+ ## Error
401
+
402
+ \`\`\`json
403
+ ${JSON.stringify(serializedError, null, 2)}
404
+ \`\`\`
405
+
406
+ ## Trace
407
+
408
+ \`\`\`json
409
+ ${JSON.stringify(traceSnapshot, null, 2)}
410
+ \`\`\`${extraSections.length ? `
411
+ ${extraSections.map((section) => `
412
+ ## ${section.heading}
413
+
414
+ ${section.markdown}`).join("\n")}
415
+ ` : "\n"}
416
+ `;
417
+ fs.writeFileSync(reportPath, report);
418
+ helperTrace("error-report:write:done", reportPath);
419
+ return reportPath;
420
+ } catch (writeError) {
421
+ console.error("\u26A0\uFE0F \uC5D0\uB7EC \uB85C\uADF8 \uD30C\uC77C \uC0DD\uC131\uC5D0 \uC2E4\uD328\uD588\uC2B5\uB2C8\uB2E4.");
422
+ console.error(writeError);
423
+ return "";
424
+ }
425
+ }
426
+ function exitWithError(message, options = {}) {
427
+ const reportPath = writeErrorReport(options.error || new Error(message), {
428
+ ...options,
429
+ title: message
430
+ });
431
+ console.error(message);
432
+ if (options.error) {
433
+ console.error(options.error);
434
+ }
435
+ if (reportPath) {
436
+ console.error(`\u{1F4C4} \uC5D0\uB7EC \uB85C\uADF8 \uC800\uC7A5 \uC704\uCE58: ${reportPath}`);
437
+ }
438
+ process.exit(1);
439
+ }
440
+ function parseServiceFromArgs(args4 = process.argv.slice(2)) {
441
+ helperTrace("parse-service:start", `args=${JSON.stringify(args4)}`);
442
+ const serviceIndex = args4.indexOf("--service");
443
+ const rawService = serviceIndex !== -1 ? args4[serviceIndex + 1] : "";
444
+ if (!rawService) {
445
+ helperTrace("parse-service:empty");
446
+ return "";
447
+ }
448
+ const normalizedService = rawService.toLowerCase();
449
+ if (AIServices.includes(normalizedService)) {
450
+ helperTrace("parse-service:resolved", normalizedService);
451
+ return normalizedService;
452
+ }
453
+ helperTrace("parse-service:invalid", rawService);
454
+ exitWithError(
455
+ `\u274C \uC9C0\uC6D0\uD558\uC9C0 \uC54A\uB294 \uC11C\uBE44\uC2A4\uC785\uB2C8\uB2E4: ${rawService}. \uC0AC\uC6A9 \uAC00\uB2A5 \uAC12: ${AIServices.join(", ")} (\uC608: --service codex)`,
456
+ {
457
+ scope: "helper:parseServiceFromArgs",
458
+ args: args4,
459
+ extraSections: [
460
+ {
461
+ heading: "Allowed Services",
462
+ markdown: `\`\`\`json
463
+ ${JSON.stringify(AIServices, null, 2)}
464
+ \`\`\``
465
+ }
466
+ ]
467
+ }
468
+ );
469
+ }
470
+ function truncateCommitSubject(subject) {
471
+ if (subject.length <= 72) {
472
+ return subject;
473
+ }
474
+ return `${subject.slice(0, 69)}...`;
475
+ }
476
+ function getRecentCommitOptions() {
477
+ const output = runGitCommand(
478
+ ["log", `-${COMMIT_FETCH_LIMIT}`, "--date=relative", "--pretty=format:%h%x09%an%x09%ar%x09%s"],
479
+ { allowFailure: true }
480
+ );
481
+ if (!output) {
482
+ return [];
483
+ }
484
+ return output.split("\n").map((line) => {
485
+ const [hash = "", author = "", relativeDate = "", ...subjectParts] = line.split(" ");
486
+ const subject = subjectParts.join(" ").trim();
487
+ return {
488
+ author,
489
+ description: `${author} | ${relativeDate}`,
490
+ hash,
491
+ label: `${hash} | ${truncateCommitSubject(subject)}`,
492
+ relativeDate,
493
+ subject
494
+ };
495
+ });
496
+ }
497
+ function buildSelectedCommitSummary(commits) {
498
+ return commits.map((commit) => `- ${commit.hash} | ${commit.subject} | ${commit.author} | ${commit.relativeDate}`).join("\n");
499
+ }
500
+ function getReviewPathspecArgs() {
501
+ const { includePatterns, excludePatterns } = getGitDiffPathspecs();
502
+ return [...includePatterns, ...excludePatterns];
503
+ }
504
+ function buildSelectedCommitDiff(commits) {
505
+ const reviewPathspecArgs = getReviewPathspecArgs();
506
+ const sections = commits.map((commit) => {
507
+ const diff = runGitCommand(["show", "--stat", "--patch", "--format=", commit.hash, "--", ...reviewPathspecArgs], {
508
+ allowFailure: true,
509
+ trimOutput: false
510
+ }).trim();
511
+ if (!diff) {
512
+ return "";
513
+ }
514
+ return [`## ${commit.hash} ${commit.subject}`, diff].join("\n\n");
515
+ }).filter(Boolean).join("\n\n");
516
+ if (!sections) {
517
+ return "";
518
+ }
519
+ return ["# \uC120\uD0DD\uD55C \uCEE4\uBC0B", buildSelectedCommitSummary(commits), "", "# \uB9AC\uBDF0 \uB300\uC0C1 diff", sections].join("\n");
520
+ }
521
+ function getSelectedCommitFiles(commits) {
522
+ const reviewPathspecArgs = getReviewPathspecArgs();
523
+ const files = /* @__PURE__ */ new Set();
524
+ commits.forEach((commit) => {
525
+ const output = runGitCommand(["show", "--pretty=format:", "--name-only", commit.hash, "--", ...reviewPathspecArgs], {
526
+ allowFailure: true
527
+ });
528
+ output.split("\n").map((line) => line.trim()).filter(Boolean).forEach((filePath) => files.add(filePath));
529
+ });
530
+ return [...files];
98
531
  }
99
532
  function openReport(reportPath) {
100
533
  const resolvedPath = path.resolve(reportPath);
101
534
  const { platform } = process;
535
+ helperTrace("open-report:start", resolvedPath);
102
536
  const openWithChrome = () => {
103
537
  if (platform === "darwin") {
104
538
  execSync(`open -a "Google Chrome" "${resolvedPath}"`, { stdio: "ignore" });
@@ -123,63 +557,188 @@ function openReport(reportPath) {
123
557
  };
124
558
  try {
125
559
  if (openWithChrome()) {
560
+ helperTrace("open-report:chrome:success", platform);
126
561
  console.log("\u{1F680} Google Chrome\uC5D0\uC11C \uB9AC\uD3EC\uD2B8\uB97C \uC5F4\uC5C8\uC2B5\uB2C8\uB2E4.");
127
562
  return;
128
563
  }
129
- } catch {
564
+ } catch (error) {
565
+ helperTrace("open-report:chrome:failed", getErrorSummary(error));
130
566
  }
131
567
  try {
132
568
  if (openWithDefaultBrowser()) {
569
+ helperTrace("open-report:default-browser:success", platform);
133
570
  console.log("\u{1F680} \uAE30\uBCF8 \uBE0C\uB77C\uC6B0\uC800\uC5D0\uC11C \uB9AC\uD3EC\uD2B8\uB97C \uC5F4\uC5C8\uC2B5\uB2C8\uB2E4.");
134
571
  return;
135
572
  }
136
- } catch (e) {
137
- console.error("\u26A0\uFE0F \uBE0C\uB77C\uC6B0\uC800 \uC5F4\uAE30 \uC2E4\uD328:", e);
573
+ } catch (error) {
574
+ helperTrace("open-report:default-browser:failed", getErrorSummary(error));
575
+ console.error("\u26A0\uFE0F \uBE0C\uB77C\uC6B0\uC800 \uC5F4\uAE30 \uC2E4\uD328:", error);
138
576
  return;
139
577
  }
578
+ helperTrace("open-report:unsupported-platform", platform);
140
579
  console.error(`\u26A0\uFE0F \uC9C0\uC6D0\uD558\uC9C0 \uC54A\uB294 \uD50C\uB7AB\uD3FC\uC785\uB2C8\uB2E4: ${platform}`);
141
580
  }
142
- function getDiffArgs() {
143
- const args4 = process.argv.slice(2);
144
- const commitIndex = args4.indexOf("--commit");
145
- const { includeParams, excludeParams } = getGitDiffFilter();
146
- let diffArgs = "";
147
- if (commitIndex !== -1) {
148
- const commitHash = args4[commitIndex + 1];
149
- if (!commitHash) {
150
- console.error("\u274C \uCEE4\uBC0B \uD574\uC2DC\uAC00 \uC81C\uACF5\uB418\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4.");
151
- process.exit(1);
581
+ function ensureInteractiveSelectionAvailable(scope, message) {
582
+ if (process.stdin.isTTY && process.stdout.isTTY && typeof process.stdin.setRawMode === "function") {
583
+ return;
584
+ }
585
+ helperTrace(`${scope}:tty-missing`);
586
+ exitWithError(message, {
587
+ scope: `helper:${scope}`
588
+ });
589
+ }
590
+ function renderSelectionBlock(lines, previousLineCount) {
591
+ if (previousLineCount > 0) {
592
+ readline.moveCursor(process.stdout, 0, -previousLineCount);
593
+ readline.clearScreenDown(process.stdout);
594
+ }
595
+ const fittedLines = fitLinesToTerminal(lines);
596
+ process.stdout.write(`${fittedLines.join("\n")}
597
+ `);
598
+ return fittedLines.length;
599
+ }
600
+ function getSelectionWindowRange(optionCount, selectedIndex, windowSize) {
601
+ if (optionCount <= windowSize) {
602
+ return {
603
+ end: optionCount,
604
+ start: 0
605
+ };
606
+ }
607
+ const halfWindow = Math.floor(windowSize / 2);
608
+ const maxStart = optionCount - windowSize;
609
+ const start = Math.max(0, Math.min(selectedIndex - halfWindow, maxStart));
610
+ return {
611
+ end: Math.min(optionCount, start + windowSize),
612
+ start
613
+ };
614
+ }
615
+ function buildMultiSelectLines(question, options, selectedIndex, toggled, windowSize) {
616
+ const { start, end } = getSelectionWindowRange(options.length, selectedIndex, windowSize);
617
+ const lines = [
618
+ `${ANSI.bold}${question}${ANSI.reset}`,
619
+ `${ANSI.dim}\u2191\u2193 \uC774\uB3D9 | Space \uC120\uD0DD/\uD574\uC81C | Enter \uC644\uB8CC | Esc \uCDE8\uC18C${ANSI.reset}`,
620
+ `${ANSI.dim}\uC120\uD0DD\uB428: ${toggled.size}\uAC1C / \uC804\uCCB4: ${options.length}\uAC1C${ANSI.reset}`
621
+ ];
622
+ for (let index = start; index < end; index += 1) {
623
+ const option = options[index];
624
+ if (!option) {
625
+ continue;
152
626
  }
153
- const nextArg = args4[commitIndex + 2];
154
- let n = 0;
155
- if (nextArg && !nextArg.startsWith("--")) {
156
- n = parseInt(nextArg, 10);
157
- if (isNaN(n)) {
158
- n = 0;
159
- }
627
+ const cursor = index === selectedIndex ? `${ANSI.cyan}>${ANSI.reset}` : " ";
628
+ const checked = toggled.has(index) ? `${ANSI.green}\u2611${ANSI.reset}` : "\u2610";
629
+ const description = option.description ? ` ${ANSI.dim}${option.description}${ANSI.reset}` : "";
630
+ lines.push(`${cursor} ${checked} ${option.label}${description}`);
631
+ }
632
+ if (options.length > windowSize) {
633
+ lines.push(`${ANSI.dim}\uD45C\uC2DC \uBC94\uC704: ${start + 1}-${end} / ${options.length}${ANSI.reset}`);
634
+ }
635
+ return lines;
636
+ }
637
+ async function showMultiSelect(question, options, windowSize = COMMIT_SELECTION_WINDOW) {
638
+ ensureInteractiveSelectionAvailable("showMultiSelect", "\u274C \uCEE4\uBC0B \uC120\uD0DD \uBAA8\uB2EC\uC740 TTY \uD658\uACBD\uC5D0\uC11C\uB9CC \uC0AC\uC6A9\uD560 \uC218 \uC788\uC2B5\uB2C8\uB2E4.");
639
+ let selectedIndex = 0;
640
+ let renderedLineCount = 0;
641
+ const toggled = /* @__PURE__ */ new Set();
642
+ const rl = readline.createInterface({
643
+ input: process.stdin,
644
+ output: process.stdout,
645
+ terminal: true
646
+ });
647
+ process.stdout.write("\x1B[?25l");
648
+ const cleanup = () => {
649
+ if (renderedLineCount > 0) {
650
+ readline.moveCursor(process.stdout, 0, -renderedLineCount);
651
+ readline.clearScreenDown(process.stdout);
652
+ renderedLineCount = 0;
160
653
  }
161
- console.log(`\u2139\uFE0F \uCEE4\uBC0B '${commitHash}' ${n > 0 ? ` \uD3EC\uD568 \uCD1D ${n + 1}\uAC1C\uC758 \uCEE4\uBC0B` : ""}\uC744 \uB9AC\uBDF0\uD569\uB2C8\uB2E4...`);
162
- diffArgs = `${commitHash}~${n + 1} ${commitHash}`;
163
- } else {
164
- try {
165
- const check = execSync(`git diff --name-only -- ${includeParams} ${excludeParams}`).toString();
166
- if (!check.trim()) {
167
- console.log("\u2139\uFE0F Unstaged \uBCC0\uACBD\uC0AC\uD56D\uC774 \uC5C6\uC2B5\uB2C8\uB2E4. \uB9C8\uC9C0\uB9C9 \uCEE4\uBC0B(HEAD)\uC744 \uB9AC\uBDF0\uD569\uB2C8\uB2E4...");
168
- diffArgs = "HEAD~1 HEAD";
654
+ process.stdin.removeListener("data", onData);
655
+ process.stdin.setRawMode(false);
656
+ process.stdin.pause();
657
+ rl.close();
658
+ process.stdout.write("\x1B[?25h");
659
+ };
660
+ const render = () => {
661
+ const lines = buildMultiSelectLines(question, options, selectedIndex, toggled, windowSize);
662
+ renderedLineCount = renderSelectionBlock(lines, renderedLineCount);
663
+ };
664
+ const confirmSelection = (resolve) => {
665
+ const values = [...toggled].sort((left, right) => left - right).map((index) => options[index]?.value).filter((value) => value !== void 0);
666
+ cleanup();
667
+ resolve(values);
668
+ };
669
+ const cancelSelection = (resolve) => {
670
+ cleanup();
671
+ resolve([]);
672
+ };
673
+ let onData = (_data) => {
674
+ };
675
+ render();
676
+ return new Promise((resolve) => {
677
+ onData = (data) => {
678
+ const key = data.toString();
679
+ if (key === "") {
680
+ cleanup();
681
+ process.exit(0);
169
682
  }
170
- } catch {
171
- }
683
+ if (key === "\x1B") {
684
+ cancelSelection(resolve);
685
+ return;
686
+ }
687
+ if (key === "\x1B[A") {
688
+ selectedIndex = (selectedIndex - 1 + options.length) % options.length;
689
+ render();
690
+ return;
691
+ }
692
+ if (key === "\x1B[B") {
693
+ selectedIndex = (selectedIndex + 1) % options.length;
694
+ render();
695
+ return;
696
+ }
697
+ if (key === " ") {
698
+ if (toggled.has(selectedIndex)) {
699
+ toggled.delete(selectedIndex);
700
+ } else {
701
+ toggled.add(selectedIndex);
702
+ }
703
+ render();
704
+ return;
705
+ }
706
+ if (key === "\r" || key === "\n") {
707
+ confirmSelection(resolve);
708
+ }
709
+ };
710
+ process.stdin.setRawMode(true);
711
+ process.stdin.resume();
712
+ process.stdin.on("data", onData);
713
+ });
714
+ }
715
+ async function selectReviewCommits() {
716
+ const commits = getRecentCommitOptions();
717
+ if (commits.length === 0) {
718
+ console.log("\u2139\uFE0F \uB9AC\uBDF0\uD560 \uCD5C\uADFC \uCEE4\uBC0B\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.");
719
+ return [];
172
720
  }
173
- return diffArgs;
721
+ return showMultiSelect(
722
+ "\uB9AC\uBDF0\uD560 \uCEE4\uBC0B\uC744 \uC120\uD0DD\uD574\uC8FC\uC138\uC694.",
723
+ commits.map((commit) => ({
724
+ description: commit.description,
725
+ label: commit.label,
726
+ value: commit
727
+ })),
728
+ COMMIT_SELECTION_WINDOW
729
+ );
174
730
  }
175
731
  async function showSelectionAIService() {
176
732
  const selectedServiceFromArgs = parseServiceFromArgs();
177
733
  if (selectedServiceFromArgs) {
734
+ helperTrace("show-selection:from-args", selectedServiceFromArgs);
178
735
  console.log(`
179
- \u2705 \x1B[32m${selectedServiceFromArgs}\x1B[0m \uC11C\uBE44\uC2A4\uAC00 \uC120\uD0DD\uB418\uC5C8\uC2B5\uB2C8\uB2E4. (--service)
736
+ \u2705 ${ANSI.green}${selectedServiceFromArgs}${ANSI.reset} \uC11C\uBE44\uC2A4\uAC00 \uC120\uD0DD\uB418\uC5C8\uC2B5\uB2C8\uB2E4. (--service)
180
737
  `);
181
738
  return selectedServiceFromArgs;
182
739
  }
740
+ ensureInteractiveSelectionAvailable("showSelectionAIService", "\u274C AI \uC11C\uBE44\uC2A4 \uC120\uD0DD UI\uB294 TTY \uD658\uACBD\uC5D0\uC11C\uB9CC \uC0AC\uC6A9\uD560 \uC218 \uC788\uC2B5\uB2C8\uB2E4.");
741
+ helperTrace("show-selection:interactive:start");
183
742
  let selectedIndex = 0;
184
743
  const rl = readline.createInterface({
185
744
  input: process.stdin,
@@ -193,13 +752,15 @@ async function showSelectionAIService() {
193
752
  readline.moveCursor(process.stdout, 0, -(AIServices.length + 1));
194
753
  }
195
754
  firstRender = false;
755
+ helperTrace("show-selection:interactive:render", AIServices[selectedIndex] || "unknown");
196
756
  readline.clearScreenDown(process.stdout);
197
757
  process.stdout.write(
198
- "\u{1F916} AI \uC11C\uBE44\uC2A4\uB97C \uC120\uD0DD\uD574\uC8FC\uC138\uC694 (\x1B[33m\u2191\u2193 \uBC29\uD5A5\uD0A4\x1B[0m \uC774\uB3D9, \x1B[33mEnter\x1B[0m \uC120\uD0DD):\n"
758
+ `\u{1F916} AI \uC11C\uBE44\uC2A4\uB97C \uC120\uD0DD\uD574\uC8FC\uC138\uC694 (${ANSI.yellow}\u2191\u2193 \uBC29\uD5A5\uD0A4${ANSI.reset} \uC774\uB3D9, ${ANSI.yellow}Enter${ANSI.reset} \uC120\uD0DD):
759
+ `
199
760
  );
200
761
  AIServices.forEach((service, index) => {
201
762
  if (index === selectedIndex) {
202
- process.stdout.write(` \x1B[36m>\x1B[0m \x1B[36m\u25C9\x1B[0m \x1B[1m${service}\x1B[0m
763
+ process.stdout.write(` ${ANSI.cyan}>${ANSI.reset} ${ANSI.cyan}\u25C9${ANSI.reset} ${ANSI.bold}${service}${ANSI.reset}
203
764
  `);
204
765
  } else {
205
766
  process.stdout.write(` \u25EF ${service}
@@ -212,6 +773,7 @@ async function showSelectionAIService() {
212
773
  const onData = (data) => {
213
774
  const key = data.toString();
214
775
  if (key === "") {
776
+ helperTrace("show-selection:interactive:ctrl-c");
215
777
  process.stdout.write("\x1B[?25h");
216
778
  process.exit(0);
217
779
  }
@@ -228,10 +790,11 @@ async function showSelectionAIService() {
228
790
  rl.close();
229
791
  process.stdout.write("\x1B[?25h");
230
792
  console.log(`
231
- \u2705 \x1B[32m${AIServices[selectedIndex]}\x1B[0m \uC11C\uBE44\uC2A4\uAC00 \uC120\uD0DD\uB418\uC5C8\uC2B5\uB2C8\uB2E4.
793
+ \u2705 ${ANSI.green}${AIServices[selectedIndex]}${ANSI.reset} \uC11C\uBE44\uC2A4\uAC00 \uC120\uD0DD\uB418\uC5C8\uC2B5\uB2C8\uB2E4.
232
794
  `);
233
795
  const result = AIServices[selectedIndex];
234
796
  if (result) {
797
+ helperTrace("show-selection:interactive:confirmed", result);
235
798
  resolve(result);
236
799
  }
237
800
  }
@@ -254,6 +817,11 @@ function getArgValue(flag) {
254
817
  }
255
818
  return args[index + 1];
256
819
  }
820
+ function printNotice(message) {
821
+ if (args.includes("--test")) {
822
+ console.warn(message);
823
+ }
824
+ }
257
825
  function toUnique(values) {
258
826
  const seen = /* @__PURE__ */ new Set();
259
827
  return values.filter((value) => {
@@ -277,11 +845,11 @@ function resolveReasoningEffort() {
277
845
  const normalized = normalizeEffort(customReasoningEffort);
278
846
  trace("reasoning:custom", `${customReasoningEffort} -> ${normalized}`);
279
847
  if (customReasoningEffort === "minimal") {
280
- console.warn("\u26A0\uFE0F Claude\uB294 minimal\uC744 \uC9C1\uC811 \uC9C0\uC6D0\uD558\uC9C0 \uC54A\uC544 low\uB85C \uB9E4\uD551\uD569\uB2C8\uB2E4.");
848
+ printNotice("\u26A0\uFE0F Claude\uB294 minimal\uC744 \uC9C1\uC811 \uC9C0\uC6D0\uD558\uC9C0 \uC54A\uC544 low\uB85C \uB9E4\uD551\uD569\uB2C8\uB2E4.");
281
849
  }
282
850
  return normalized;
283
851
  }
284
- console.warn(
852
+ printNotice(
285
853
  `\u26A0\uFE0F \uC9C0\uC6D0\uB418\uC9C0 \uC54A\uB294 reasoning effort(${customReasoningEffort})\uC785\uB2C8\uB2E4. allowed: ${ALLOWED_REASONING_EFFORTS.join(
286
854
  ", "
287
855
  )}`
@@ -324,7 +892,7 @@ function buildClaudeExecCommand(options) {
324
892
  const modelOption = model ? `--model ${shellQuote(model)}` : "";
325
893
  const fallbackOption = model && fallbackModel ? `--fallback-model ${shellQuote(fallbackModel)}` : "";
326
894
  const effortOption = `--effort ${shellQuote(effort)}`;
327
- const appendedPromptFiles = systemPromptFiles.map((path2) => `--append-system-prompt-file ${shellQuote(path2)}`).join(" ");
895
+ const appendedPromptFiles = systemPromptFiles.map((path3) => `--append-system-prompt-file ${shellQuote(path3)}`).join(" ");
328
896
  return `cat ${shellQuote(tempDiffPath2)} | claude ${[
329
897
  modelOption,
330
898
  fallbackOption,
@@ -351,16 +919,19 @@ var createClaudeCommand = (tempDiffPath2, reviewFormPath2) => {
351
919
  trace("reviewForm:status", reviewFormExists ? "exists" : "missing");
352
920
  const systemPromptFiles = reviewFormExists ? [...existingRuleFiles, reviewFormPath2] : existingRuleFiles;
353
921
  const prompt = "\uC704 \uADDC\uCE59\uB4E4\uC744 \uCC38\uACE0\uD558\uC5EC \uC774 diff\uB97C \uCF54\uB4DC\uB9AC\uBDF0\uD574\uC918. \uB9AC\uBDF0\uC591\uC2DD\uC5D0 \uB9DE\uCDB0\uC11C \uC791\uC131\uD574\uC918.";
922
+ trace("prompt:prepared", `length=${prompt.length}`);
923
+ trace("system-prompt-files", `count=${systemPromptFiles.length}`);
354
924
  const modelCandidates = toUnique(customModel ? [customModel, ...aliasFallbacks] : aliasFallbacks);
355
925
  trace("model:candidates", modelCandidates.join(", "));
926
+ trace("command:candidates:count", String(modelCandidates.length + 1));
356
927
  if (customModel) {
357
- console.warn(
928
+ printNotice(
358
929
  `\u26A0\uFE0F \uCEE4\uC2A4\uD140 \uBAA8\uB378(${customModel})\uC744 \uC6B0\uC120 \uC2DC\uB3C4\uD558\uACE0 \uC2E4\uD328\uD558\uBA74 alias(${aliasFallbacks.join(
359
930
  " -> "
360
931
  )}) \uBC0F \uAE30\uBCF8 \uBAA8\uB378 \uC21C\uC73C\uB85C \uD3F4\uBC31\uD569\uB2C8\uB2E4.`
361
932
  );
362
933
  } else {
363
- console.warn(
934
+ printNotice(
364
935
  `\u26A0\uFE0F \uBAA8\uB378\uC774 \uC9C0\uC815\uB418\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4. alias(${aliasFallbacks.join(" -> ")})\uB97C \uC21C\uCC28 \uC2DC\uB3C4\uD558\uACE0 \uB9C8\uC9C0\uB9C9\uC5D0 \uAE30\uBCF8 \uBAA8\uB378\uB85C \uD3F4\uBC31\uD569\uB2C8\uB2E4.`
365
936
  );
366
937
  }
@@ -393,6 +964,7 @@ var createClaudeCommand = (tempDiffPath2, reviewFormPath2) => {
393
964
  \uC0DD\uC131\uB420 \uBA85\uB839\uC5B4 \uBBF8\uB9AC\uBCF4\uAE30:
394
965
  ${safeCommand}"`;
395
966
  }
967
+ trace("command:mode", "execute");
396
968
  trace("createClaudeCommand:end");
397
969
  return command;
398
970
  };
@@ -403,23 +975,25 @@ function checkClaudeCliInstalled() {
403
975
  trace2("version-check:run", "claude --version");
404
976
  execSync("claude --version", { stdio: "ignore" });
405
977
  trace2("version-check:ok");
406
- } catch {
407
- trace2("version-check:failed", "install-start");
978
+ } catch (error) {
979
+ trace2("version-check:failed", getErrorSummary(error));
980
+ trace2("install:start", "@anthropic-ai/claude-code");
408
981
  console.log(
409
982
  "\u2139\uFE0F claude-cli\uAC00 \uC124\uCE58\uB418\uC5B4 \uC788\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4. \uC124\uCE58\uB97C \uC9C4\uD589\uD569\uB2C8\uB2E4... npm install -g @anthropic-ai/claude-code"
410
983
  );
411
984
  try {
412
985
  execSync("npm install -g @anthropic-ai/claude-code", { stdio: "inherit" });
413
- trace2("install:ok", "exit(1) for login");
986
+ trace2("install:ok", "login-required");
414
987
  console.log("\u2705 claude-cli \uC124\uCE58\uAC00 \uC644\uB8CC\uB418\uC5C8\uC2B5\uB2C8\uB2E4.");
415
988
  console.log("\u26A0\uFE0F claude-cli \uC0AC\uC6A9\uC744 \uC704\uD574 \uC778\uC99D\uC774 \uD544\uC694\uD569\uB2C8\uB2E4.");
416
989
  console.log(' \uD130\uBBF8\uB110\uC5D0\uC11C "claude" \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.');
417
990
  process.exit(1);
418
991
  } catch (installError) {
419
- trace2("install:failed");
420
- console.error("\u274C claude-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).");
421
- console.error(installError);
422
- process.exit(1);
992
+ trace2("install:failed", getErrorSummary(installError));
993
+ exitWithError("\u274C claude-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).", {
994
+ scope: "installation-claude",
995
+ error: installError
996
+ });
423
997
  }
424
998
  }
425
999
  trace2("checkClaudeCliInstalled:end");
@@ -437,6 +1011,11 @@ function getArgValue2(flag) {
437
1011
  }
438
1012
  return args2[index + 1];
439
1013
  }
1014
+ function printNotice2(message) {
1015
+ if (args2.includes("--test")) {
1016
+ console.warn(message);
1017
+ }
1018
+ }
440
1019
  function resolveReasoningEffort2() {
441
1020
  const customReasoningEffort = getArgValue2("--reasoning-effort");
442
1021
  if (customReasoningEffort) {
@@ -444,7 +1023,7 @@ function resolveReasoningEffort2() {
444
1023
  trace3("reasoning:custom", customReasoningEffort);
445
1024
  return customReasoningEffort;
446
1025
  }
447
- console.warn(
1026
+ printNotice2(
448
1027
  `\u26A0\uFE0F \uC9C0\uC6D0\uB418\uC9C0 \uC54A\uB294 reasoning effort(${customReasoningEffort})\uC785\uB2C8\uB2E4. allowed: ${ALLOWED_REASONING_EFFORTS2.join(
449
1028
  ", "
450
1029
  )}`
@@ -485,16 +1064,17 @@ ${reviewFormLine || "- (\uC5C6\uC74C)"}
485
1064
  - ${tempDiffPath2}
486
1065
 
487
1066
  \uBC18\uB4DC\uC2DC \uB9AC\uBDF0 \uC591\uC2DD\uC5D0 \uB9DE\uCDB0 \uC791\uC131\uD574\uC918.`;
1067
+ trace3("prompt:prepared", `length=${prompt.length}`);
488
1068
  let command = "";
489
1069
  if (customModel) {
490
- 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.");
1070
+ printNotice2("\u26A0\uFE0F \uC9C0\uC815\uD55C \uBAA8\uB378\uC774 \uC5C6\uB294 \uACBD\uC6B0, \uC5D0\uB7EC\uAC00 \uBC1C\uC0DD\uD558\uB2C8 \uC8FC\uC758\uD558\uC138\uC694.");
491
1071
  trace3("model:custom", customModel);
492
1072
  command = buildCodexExecCommand(prompt, reasoningEffort, customModel);
493
1073
  } else {
494
1074
  const preferredModelAlias = "gpt-5";
495
1075
  const aliasCommand = buildCodexExecCommand(prompt, reasoningEffort, preferredModelAlias);
496
1076
  const fallbackCommand = buildCodexExecCommand(prompt, reasoningEffort);
497
- console.warn(
1077
+ printNotice2(
498
1078
  `\u26A0\uFE0F \uBAA8\uB378\uC774 \uC9C0\uC815\uB418\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4. alias(${preferredModelAlias})\uB97C \uC6B0\uC120 \uC2DC\uB3C4\uD558\uACE0 \uC2E4\uD328\uD558\uBA74 \uACC4\uC815 \uAE30\uBCF8 \uBAA8\uB378\uB85C \uC790\uB3D9 \uD3F4\uBC31\uD569\uB2C8\uB2E4.`
499
1079
  );
500
1080
  trace3("model:alias-first", preferredModelAlias);
@@ -510,6 +1090,7 @@ ${reviewFormLine || "- (\uC5C6\uC74C)"}
510
1090
  \uC0DD\uC131\uB420 \uBA85\uB839\uC5B4 \uBBF8\uB9AC\uBCF4\uAE30:
511
1091
  ${safeCommand}"`;
512
1092
  }
1093
+ trace3("command:mode", "execute");
513
1094
  trace3("createCodexCommand:end");
514
1095
  return command;
515
1096
  };
@@ -520,21 +1101,23 @@ function checkCodexCliInstalled() {
520
1101
  trace4("version-check:run", "codex --version");
521
1102
  execSync("codex --version", { stdio: "ignore" });
522
1103
  trace4("version-check:ok");
523
- } catch {
524
- trace4("version-check:failed", "install-start");
1104
+ } catch (error) {
1105
+ trace4("version-check:failed", getErrorSummary(error));
1106
+ trace4("install:start", "@openai/codex");
525
1107
  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");
526
1108
  try {
527
1109
  execSync("npm install -g @openai/codex", { stdio: "inherit" });
528
- trace4("install:ok", "exit(1) for login");
1110
+ trace4("install:ok", "login-required");
529
1111
  console.log("\u2705 codex-cli \uC124\uCE58\uAC00 \uC644\uB8CC\uB418\uC5C8\uC2B5\uB2C8\uB2E4.");
530
1112
  console.log("\u26A0\uFE0F codex-cli \uC0AC\uC6A9\uC744 \uC704\uD574 \uC778\uC99D\uC774 \uD544\uC694\uD569\uB2C8\uB2E4.");
531
1113
  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.');
532
1114
  process.exit(1);
533
1115
  } catch (installError) {
534
- trace4("install:failed");
535
- 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).");
536
- console.error(installError);
537
- process.exit(1);
1116
+ trace4("install:failed", getErrorSummary(installError));
1117
+ exitWithError("\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).", {
1118
+ scope: "installation-codex",
1119
+ error: installError
1120
+ });
538
1121
  }
539
1122
  }
540
1123
  trace4("checkCodexCliInstalled:end");
@@ -552,6 +1135,11 @@ function getArgValue3(flag) {
552
1135
  }
553
1136
  return args3[index + 1];
554
1137
  }
1138
+ function printNotice3(message) {
1139
+ if (args3.includes("--test")) {
1140
+ console.warn(message);
1141
+ }
1142
+ }
555
1143
  function toUnique2(values) {
556
1144
  const seen = /* @__PURE__ */ new Set();
557
1145
  return values.filter((value) => {
@@ -569,7 +1157,7 @@ function resolveReasoningEffort3() {
569
1157
  trace5("reasoning:custom", customReasoningEffort);
570
1158
  return customReasoningEffort;
571
1159
  }
572
- console.warn(
1160
+ printNotice3(
573
1161
  `\u26A0\uFE0F \uC9C0\uC6D0\uB418\uC9C0 \uC54A\uB294 reasoning effort(${customReasoningEffort})\uC785\uB2C8\uB2E4. allowed: ${ALLOWED_REASONING_EFFORTS3.join(
574
1162
  ", "
575
1163
  )}`
@@ -631,36 +1219,57 @@ function buildGeminiExecCommand(prompt, model) {
631
1219
  const modelOption = model ? `--model ${shellQuote3(model)}` : "";
632
1220
  return `gemini ${[modelOption, "-p", shellQuote3(prompt)].filter(Boolean).join(" ")}`;
633
1221
  }
1222
+ function toGeminiFileReference(filePath) {
1223
+ return `@${filePath}`;
1224
+ }
1225
+ function buildGeminiFileReferenceSection(files) {
1226
+ const existingFiles = files.filter((file) => fs.existsSync(file.path));
1227
+ return {
1228
+ count: existingFiles.length,
1229
+ lines: existingFiles.map((file) => `- ${file.display}: ${toGeminiFileReference(file.path)}`)
1230
+ };
1231
+ }
634
1232
  var createGeminiCommand = (tempDiffPath2, reviewFormPath2) => {
635
1233
  trace5("createGeminiCommand:start", `tempDiffPath=${tempDiffPath2}, reviewFormPath=${reviewFormPath2}`);
636
1234
  const customModel = getArgValue3("--model");
637
1235
  const reasoningEffort = resolveReasoningEffort3();
638
1236
  const primaryAlias = resolvePrimaryAlias2(reasoningEffort);
639
1237
  const aliasFallbacks = toUnique2(getAliasFallbacks2(primaryAlias));
1238
+ const resolvedTempDiffPath = path.resolve(tempDiffPath2);
1239
+ const resolvedReviewFormPath = path.resolve(reviewFormPath2);
640
1240
  const rules = [
641
1241
  { path: rulesPath, display: "\uB8F0\uC14B" },
642
1242
  { path: namingRulesPath, display: "\uB124\uC774\uBC0D \uADDC\uCE59" },
643
1243
  { path: codingConventionRulesPath, display: "\uCF54\uB529 \uCEE8\uBCA4\uC158" }
644
1244
  ];
645
- const validRules = rules.filter((rule) => fs.existsSync(rule.path)).map((rule) => `@${rule.path}`).join(", ");
646
- const rulesCount = validRules ? validRules.split(",").length : 0;
647
- trace5("rules:loaded", `count=${rulesCount}`);
648
- const reviewFormRef = fs.existsSync(reviewFormPath2) ? `@${reviewFormPath2}` : "(\uC5C6\uC74C)";
649
- trace5("reviewForm:status", reviewFormRef === "(\uC5C6\uC74C)" ? "missing" : "exists");
1245
+ const ruleSection = buildGeminiFileReferenceSection(rules);
1246
+ trace5("rules:loaded", `count=${ruleSection.count}`);
1247
+ const reviewFormExists = fs.existsSync(resolvedReviewFormPath);
1248
+ const reviewFormLine = reviewFormExists ? `- \uB9AC\uBDF0 \uC591\uC2DD: ${toGeminiFileReference(resolvedReviewFormPath)}` : "- \uB9AC\uBDF0 \uC591\uC2DD: (\uC5C6\uC74C)";
1249
+ trace5("reviewForm:status", reviewFormExists ? "exists" : "missing");
650
1250
  const reasoningInstruction = getReasoningInstruction(reasoningEffort);
651
- const prompt = `\uB2E4\uC74C \uADDC\uCE59\uB4E4\uC744 \uCC38\uACE0\uD574\uC11C(${validRules || "(\uC5C6\uC74C)"}) \uC774 diff(@${tempDiffPath2})\uB97C \uB9AC\uBDF0\uD574\uC918.
652
- \uB9AC\uBDF0 \uC591\uC2DD\uC740 ${reviewFormRef} \uC5D0 \uB9DE\uCDB0\uC11C \uC791\uC131\uD574\uC918.
1251
+ const prompt = `\uC544\uB798 \uD30C\uC77C\uB4E4\uC744 \uC21C\uC11C\uB300\uB85C \uC77D\uACE0 \uCF54\uB4DC \uB9AC\uBDF0\uB97C \uC9C4\uD589\uD574\uC918.
1252
+ \uADDC\uCE59 \uD30C\uC77C:
1253
+ ${ruleSection.lines.join("\n") || "- (\uC5C6\uC74C)"}
1254
+ \uB9AC\uBDF0 \uC591\uC2DD \uD30C\uC77C:
1255
+ ${reviewFormLine}
1256
+ \uB9AC\uBDF0 \uB300\uC0C1 diff \uD30C\uC77C:
1257
+ - \uB9AC\uBDF0 \uB300\uC0C1 diff: ${toGeminiFileReference(resolvedTempDiffPath)}
1258
+
1259
+ \uBC18\uB4DC\uC2DC \uB9AC\uBDF0 \uC591\uC2DD\uC5D0 \uB9DE\uCDB0 \uC791\uC131\uD574\uC918.
653
1260
  \uCD94\uB860 \uAC15\uB3C4 \uC9C0\uCE68: ${reasoningInstruction}`;
1261
+ trace5("prompt:prepared", `length=${prompt.length}`);
654
1262
  const modelCandidates = toUnique2(customModel ? [customModel, ...aliasFallbacks] : aliasFallbacks);
655
1263
  trace5("model:candidates", modelCandidates.join(", "));
1264
+ trace5("command:candidates:count", String(modelCandidates.length + 1));
656
1265
  if (customModel) {
657
- console.warn(
1266
+ printNotice3(
658
1267
  `\u26A0\uFE0F \uCEE4\uC2A4\uD140 \uBAA8\uB378(${customModel})\uC744 \uC6B0\uC120 \uC2DC\uB3C4\uD558\uACE0 \uC2E4\uD328\uD558\uBA74 alias(${aliasFallbacks.join(
659
1268
  " -> "
660
1269
  )}) \uBC0F \uAE30\uBCF8 \uBAA8\uB378 \uC21C\uC73C\uB85C \uD3F4\uBC31\uD569\uB2C8\uB2E4.`
661
1270
  );
662
1271
  } else {
663
- console.warn(
1272
+ printNotice3(
664
1273
  `\u26A0\uFE0F \uBAA8\uB378\uC774 \uC9C0\uC815\uB418\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4. alias(${aliasFallbacks.join(" -> ")})\uB97C \uC21C\uCC28 \uC2DC\uB3C4\uD558\uACE0 \uB9C8\uC9C0\uB9C9\uC5D0 \uAE30\uBCF8 \uBAA8\uB378\uB85C \uD3F4\uBC31\uD569\uB2C8\uB2E4.`
665
1274
  );
666
1275
  }
@@ -675,6 +1284,7 @@ var createGeminiCommand = (tempDiffPath2, reviewFormPath2) => {
675
1284
  \uC0DD\uC131\uB420 \uBA85\uB839\uC5B4 \uBBF8\uB9AC\uBCF4\uAE30:
676
1285
  ${safeCommand}"`;
677
1286
  }
1287
+ trace5("command:mode", "execute");
678
1288
  trace5("createGeminiCommand:end");
679
1289
  return command;
680
1290
  };
@@ -685,21 +1295,23 @@ function checkGeminiCliInstalled() {
685
1295
  trace6("version-check:run", "gemini --version");
686
1296
  execSync("gemini --version", { stdio: "ignore" });
687
1297
  trace6("version-check:ok");
688
- } catch {
689
- trace6("version-check:failed", "install-start");
1298
+ } catch (error) {
1299
+ trace6("version-check:failed", getErrorSummary(error));
1300
+ trace6("install:start", "@google/gemini-cli");
690
1301
  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");
691
1302
  try {
692
1303
  execSync("npm install -g @google/gemini-cli", { stdio: "inherit" });
693
- trace6("install:ok", "exit(1) for login");
1304
+ trace6("install:ok", "login-required");
694
1305
  console.log("\u2705 gemini-cli \uC124\uCE58\uAC00 \uC644\uB8CC\uB418\uC5C8\uC2B5\uB2C8\uB2E4.");
695
1306
  console.log("\u26A0\uFE0F Gemini API \uC0AC\uC6A9\uC744 \uC704\uD574 \uC778\uC99D\uC774 \uD544\uC694\uD569\uB2C8\uB2E4.");
696
1307
  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.');
697
1308
  process.exit(1);
698
1309
  } catch (installError) {
699
- trace6("install:failed");
700
- 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).");
701
- console.error(installError);
702
- process.exit(1);
1310
+ trace6("install:failed", getErrorSummary(installError));
1311
+ 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).", {
1312
+ scope: "installation-gemini",
1313
+ error: installError
1314
+ });
703
1315
  }
704
1316
  }
705
1317
  trace6("checkGeminiCliInstalled:end");
@@ -708,64 +1320,74 @@ function checkGeminiCliInstalled() {
708
1320
  // src/pr-review/review.ts
709
1321
  async function main() {
710
1322
  const args4 = process.argv.slice(2);
1323
+ clearTraceMessages();
711
1324
  const isTest = isTestMode(args4);
712
1325
  const trace7 = createTraceLogger("review", args4);
713
1326
  trace7("main:start", `args=${JSON.stringify(args4)}`);
714
- trace7("service-selection:start");
715
- const service = await showSelectionAIService();
716
- trace7("service-selection:done", `service=${service}`);
717
- switch (service) {
718
- case "gemini":
719
- trace7("install-check:start", "service=gemini");
720
- checkGeminiCliInstalled();
721
- trace7("install-check:done", "service=gemini");
722
- break;
723
- case "claude":
724
- trace7("install-check:start", "service=claude");
725
- checkClaudeCliInstalled();
726
- trace7("install-check:done", "service=claude");
727
- break;
728
- case "codex":
729
- trace7("install-check:start", "service=codex");
730
- checkCodexCliInstalled();
731
- trace7("install-check:done", "service=codex");
732
- break;
733
- }
1327
+ let command = "";
1328
+ let savedDiffPath = "";
1329
+ let savedReportPath = "";
1330
+ let service = "";
1331
+ let selectedCommitSummary = "";
1332
+ let reviewTargetFiles = [];
734
1333
  try {
1334
+ trace7("service-selection:start");
1335
+ service = await showSelectionAIService();
1336
+ trace7("service-selection:done", `service=${service}`);
1337
+ switch (service) {
1338
+ case "gemini":
1339
+ trace7("install-check:start", "service=gemini");
1340
+ checkGeminiCliInstalled();
1341
+ trace7("install-check:done", "service=gemini");
1342
+ break;
1343
+ case "claude":
1344
+ trace7("install-check:start", "service=claude");
1345
+ checkClaudeCliInstalled();
1346
+ trace7("install-check:done", "service=claude");
1347
+ break;
1348
+ case "codex":
1349
+ trace7("install-check:start", "service=codex");
1350
+ checkCodexCliInstalled();
1351
+ trace7("install-check:done", "service=codex");
1352
+ break;
1353
+ }
735
1354
  trace7("review-flow:start");
736
1355
  console.log("\u{1F680} AI Code Review\uB97C \uC2DC\uC791\uD569\uB2C8\uB2E4...");
737
- const nowStr = getNowString();
738
- trace7("timestamp:created", nowStr);
739
- trace7("report-dir:create:start");
740
- createReportDirectory();
741
- trace7("report-dir:create:done");
742
- trace7("diff-args:build:start");
743
- const diffArgs = getDiffArgs();
744
- trace7("diff-args:build:done", `diffArgs=${diffArgs || "(default)"}`);
745
- let diff = "";
746
- const { includeParams, excludeParams } = getGitDiffFilter();
747
- trace7("diff-filter:loaded", `include=${includeParams} | exclude=${excludeParams}`);
748
- try {
749
- trace7("git-diff:run");
750
- diff = execSync(`git diff ${diffArgs} -- ${includeParams} ${excludeParams}`).toString();
751
- trace7("git-diff:done", `length=${diff.length}`);
752
- } catch {
753
- trace7("git-diff:error", "fallback-empty-diff");
1356
+ trace7("commit-selection:start");
1357
+ const selectedCommits = await selectReviewCommits();
1358
+ trace7("commit-selection:done", `count=${selectedCommits.length}`);
1359
+ if (selectedCommits.length === 0) {
1360
+ trace7("commit-selection:empty");
1361
+ console.log("\u2139\uFE0F \uC120\uD0DD\uB41C \uCEE4\uBC0B\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.");
1362
+ deleteTempDiff();
1363
+ process.exit(0);
754
1364
  }
1365
+ selectedCommitSummary = buildSelectedCommitSummary(selectedCommits);
1366
+ trace7("commit-summary:prepared", selectedCommitSummary);
1367
+ reviewTargetFiles = getSelectedCommitFiles(selectedCommits);
1368
+ console.log(`\u{1F4C2} \uB9AC\uBDF0 \uB300\uC0C1 \uD30C\uC77C(${reviewTargetFiles.length}\uAC1C): ${formatReviewTargetFiles(reviewTargetFiles)}`);
1369
+ console.log("\u23F3 \uC120\uD0DD\uD55C \uCEE4\uBC0B diff\uB97C \uC815\uB9AC\uD558\uB294 \uC911\uC785\uB2C8\uB2E4...");
1370
+ trace7("git-diff:build:start");
1371
+ const diff = buildSelectedCommitDiff(selectedCommits);
1372
+ trace7("git-diff:build:done", `length=${diff.length}`);
755
1373
  if (!diff.trim() && !isTest) {
756
1374
  trace7("empty-diff:exit");
757
- console.log("\u2139\uFE0F \uB9AC\uBDF0\uD560 \uBCC0\uACBD \uC0AC\uD56D\uC774 \uC5C6\uC2B5\uB2C8\uB2E4 (Unstaged Empty & HEAD Empty).");
1375
+ console.log("\u2139\uFE0F \uC120\uD0DD\uD55C \uCEE4\uBC0B\uC5D0\uC11C \uB9AC\uBDF0 \uB300\uC0C1 \uD30C\uC77C \uBCC0\uACBD\uC744 \uCC3E\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4.");
758
1376
  deleteTempDiff();
759
1377
  process.exit(0);
760
1378
  }
1379
+ const nowStr = getNowString();
1380
+ trace7("timestamp:created", nowStr);
1381
+ trace7("report-dir:create:start");
1382
+ createReportDirectory();
1383
+ trace7("report-dir:create:done");
761
1384
  trace7("temp-diff:write:start", tempDiffPath);
762
1385
  fs.writeFileSync(tempDiffPath, diff);
763
1386
  trace7("temp-diff:write:done");
764
1387
  trace7("saved-diff:copy:start");
765
- const savedDiffPath = getNextFilePath(REPORT_DIR, `${nowStr}-diff`, ".txt");
1388
+ savedDiffPath = getNextFilePath(REPORT_DIR, `${nowStr}-diff`, ".txt");
766
1389
  fs.copyFileSync(tempDiffPath, savedDiffPath);
767
1390
  trace7("saved-diff:copy:done", savedDiffPath);
768
- let command = "";
769
1391
  trace7("command:create:start", `service=${service}`);
770
1392
  switch (service) {
771
1393
  case "gemini":
@@ -780,11 +1402,13 @@ async function main() {
780
1402
  }
781
1403
  trace7("command:create:done");
782
1404
  trace7("command:exec:start");
783
- const result = execSync(command).toString();
1405
+ const result = (await executeShellCommandWithProgress(command, {
1406
+ progressMessage: `\u23F3 [\uB9AC\uBDF0 \uC9C4\uD589] ${service} | \uB300\uC0C1 ${reviewTargetFiles.length}\uAC1C \uD30C\uC77C`,
1407
+ streamOutput: isTest
1408
+ })).stdout;
784
1409
  trace7("command:exec:done", `resultLength=${result.length}`);
785
- console.log(result);
786
1410
  trace7("report:write:start");
787
- const savedReportPath = getNextFilePath(REPORT_DIR, nowStr, ".md");
1411
+ savedReportPath = getNextFilePath(REPORT_DIR, nowStr, ".md");
788
1412
  fs.writeFileSync(savedReportPath, result);
789
1413
  trace7("report:write:done", savedReportPath);
790
1414
  if (isTest) {
@@ -808,12 +1432,47 @@ ${command}`);
808
1432
  trace7("cleanup-temp-diff:done");
809
1433
  trace7("review-flow:end");
810
1434
  } catch (error) {
811
- trace7("review-flow:catch");
1435
+ trace7("review-flow:catch", getErrorSummary(error));
1436
+ let errorReportPath = "";
1437
+ trace7("cleanup-temp-diff:start(catch)");
1438
+ try {
1439
+ deleteTempDiff();
1440
+ trace7("cleanup-temp-diff:done(catch)");
1441
+ } catch (cleanupError) {
1442
+ trace7("cleanup-temp-diff:failed(catch)", getErrorSummary(cleanupError));
1443
+ console.error("\u26A0\uFE0F \uC784\uC2DC diff \uC815\uB9AC \uC911 \uC624\uB958\uAC00 \uBC1C\uC0DD\uD588\uC2B5\uB2C8\uB2E4.");
1444
+ console.error(cleanupError);
1445
+ }
1446
+ trace7("error-report:prepare", `service=${service}`);
1447
+ errorReportPath = writeErrorReport(error, {
1448
+ scope: "review",
1449
+ args: args4,
1450
+ extraSections: [
1451
+ {
1452
+ heading: "Execution Context",
1453
+ markdown: `\`\`\`json
1454
+ ${JSON.stringify(
1455
+ {
1456
+ service,
1457
+ selectedCommitSummary: selectedCommitSummary || null,
1458
+ reviewTargetFiles,
1459
+ command: command || null,
1460
+ tempDiffPath,
1461
+ savedDiffPath: savedDiffPath || null,
1462
+ savedReportPath: savedReportPath || null
1463
+ },
1464
+ null,
1465
+ 2
1466
+ )}
1467
+ \`\`\``
1468
+ }
1469
+ ]
1470
+ });
812
1471
  console.error("\u274C \uB9AC\uBDF0 \uB3C4\uC911 \uC624\uB958\uAC00 \uBC1C\uC0DD\uD588\uC2B5\uB2C8\uB2E4.");
813
1472
  console.error(error);
814
- trace7("cleanup-temp-diff:start(catch)");
815
- deleteTempDiff();
816
- trace7("cleanup-temp-diff:done(catch)");
1473
+ if (errorReportPath) {
1474
+ console.error(`\u{1F4C4} \uC5D0\uB7EC \uB85C\uADF8 \uC800\uC7A5 \uC704\uCE58: ${errorReportPath}`);
1475
+ }
817
1476
  process.exit(1);
818
1477
  }
819
1478
  }