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,20 +1,59 @@
1
1
  #!/usr/bin/env node
2
- import { exec, execSync } from 'child_process';
3
- import fs5 from 'fs';
4
- import util from 'util';
2
+ import { exec, execSync, execFileSync } from 'child_process';
3
+ import fs from 'fs';
4
+ import util, { inspect } from 'util';
5
5
  import path from 'path';
6
6
  import readline from 'readline';
7
7
  import { fileURLToPath } from 'url';
8
8
 
9
9
  var __dirname = path.dirname(fileURLToPath(import.meta.url));
10
- var rulesPath = path.resolve(__dirname, "../../src/common/rules/review-rules.md");
11
- var namingRulesPath = path.resolve(__dirname, "../../src/common/rules/naming-rule.md");
12
- var codingConventionRulesPath = path.resolve(__dirname, "../../src/common/rules/coding-convention.md");
13
- path.resolve(__dirname, "../../src/common/form/review-form.md");
14
- var reviewFormOneByOnePath = 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
+ resolvePackageAssetPath("src/common/form/review-form.md");
51
+ var reviewFormOneByOnePath = resolvePackageAssetPath("src/common/form/review-form-one-by-one.md");
15
52
  var REPORT_DIR = ".review-report";
16
53
  var tempDiffPath = "temp_diff.txt";
17
54
  var AIServices = ["gemini", "claude", "codex"];
55
+ var COMMIT_FETCH_LIMIT = 20;
56
+ var COMMIT_SELECTION_WINDOW = 8;
18
57
  var ignoreList = [
19
58
  "package.json",
20
59
  "*.yml",
@@ -29,77 +68,439 @@ var ignoreList = [
29
68
  ".review-report/"
30
69
  // 생성되는 리포트 폴더도 제외
31
70
  ];
32
- function parseServiceFromArgs(args4 = process.argv.slice(2)) {
33
- const serviceIndex = args4.indexOf("--service");
34
- const rawService = serviceIndex !== -1 ? args4[serviceIndex + 1] : "";
35
- 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) {
36
154
  return "";
37
155
  }
38
- const normalizedService = rawService.toLowerCase();
39
- if (AIServices.includes(normalizedService)) {
40
- 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;
41
160
  }
42
- console.error(
43
- `\u274C \uC9C0\uC6D0\uD558\uC9C0 \uC54A\uB294 \uC11C\uBE44\uC2A4\uC785\uB2C8\uB2E4: ${rawService}. \uC0AC\uC6A9 \uAC00\uB2A5 \uAC12: ${AIServices.join(", ")} (\uC608: --service codex)`
44
- );
45
- 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}`;
46
178
  }
47
- function isTestMode(args4 = process.argv.slice(2)) {
48
- 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
+ function formatReviewTargetFiles(files, visibleCount = 5) {
201
+ if (files.length === 0) {
202
+ return "(\uC5C6\uC74C)";
203
+ }
204
+ const visibleFiles = files.slice(0, visibleCount);
205
+ const hiddenCount = Math.max(0, files.length - visibleFiles.length);
206
+ if (hiddenCount === 0) {
207
+ return visibleFiles.join(", ");
208
+ }
209
+ return `${visibleFiles.join(", ")} \uC678 ${hiddenCount}\uAC1C`;
49
210
  }
50
211
  function createTraceLogger(scope, args4 = process.argv.slice(2)) {
51
212
  const enabled = isTestMode(args4);
52
213
  return (step, detail) => {
214
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
215
+ const message = `[${timestamp}][TRACE][${scope}] ${step}${detail ? ` | ${detail}` : ""}`;
216
+ traceMessages.push(message);
53
217
  if (!enabled) {
54
218
  return;
55
219
  }
56
- console.log(`[TRACE][${scope}] ${step}${detail ? ` | ${detail}` : ""}`);
220
+ console.log(message);
57
221
  };
58
222
  }
223
+ var helperTrace = createTraceLogger("helper");
224
+ function getTimestampParts(now = /* @__PURE__ */ new Date()) {
225
+ return {
226
+ YYYY: now.getFullYear(),
227
+ MM: String(now.getMonth() + 1).padStart(2, "0"),
228
+ DD: String(now.getDate()).padStart(2, "0"),
229
+ HH: String(now.getHours()).padStart(2, "0"),
230
+ mm: String(now.getMinutes()).padStart(2, "0"),
231
+ ss: String(now.getSeconds()).padStart(2, "0")
232
+ };
233
+ }
234
+ function getHumanReadableNowString(now = /* @__PURE__ */ new Date()) {
235
+ const { YYYY, MM, DD, HH, mm, ss } = getTimestampParts(now);
236
+ return `${YYYY}-${MM}-${DD} ${HH}:${mm}:${ss}`;
237
+ }
238
+ function stringifyUnknown(value) {
239
+ if (value === void 0 || value === null) {
240
+ return "";
241
+ }
242
+ if (typeof value === "string") {
243
+ return value;
244
+ }
245
+ if (Buffer.isBuffer(value)) {
246
+ return value.toString();
247
+ }
248
+ if (value instanceof Error) {
249
+ return value.stack || value.message;
250
+ }
251
+ return inspect(value, { depth: 5, breakLength: 120 });
252
+ }
253
+ function getErrorSummary(error) {
254
+ if (error instanceof Error) {
255
+ return `${error.name}: ${error.message}`;
256
+ }
257
+ return stringifyUnknown(error) || "Unknown error";
258
+ }
259
+ function serializeError(error) {
260
+ const serialized = {
261
+ summary: getErrorSummary(error)
262
+ };
263
+ if (error instanceof Error) {
264
+ serialized.name = error.name;
265
+ serialized.message = error.message;
266
+ serialized.stack = error.stack;
267
+ } else {
268
+ serialized.value = stringifyUnknown(error);
269
+ }
270
+ if (error && typeof error === "object") {
271
+ const errorLike = error;
272
+ const extraKeys = ["code", "errno", "syscall", "path", "cmd", "status", "signal", "spawnargs"];
273
+ extraKeys.forEach((key) => {
274
+ if (errorLike[key] !== void 0) {
275
+ serialized[key] = errorLike[key];
276
+ }
277
+ });
278
+ const stdout = stringifyUnknown(errorLike.stdout);
279
+ if (stdout) {
280
+ serialized.stdout = stdout;
281
+ }
282
+ const stderr = stringifyUnknown(errorLike.stderr);
283
+ if (stderr) {
284
+ serialized.stderr = stderr;
285
+ }
286
+ const cause = stringifyUnknown(errorLike.cause);
287
+ if (cause) {
288
+ serialized.cause = cause;
289
+ }
290
+ }
291
+ return serialized;
292
+ }
59
293
  function getNextFilePath(dir, baseName, extension) {
60
294
  let counter = 1;
61
295
  while (true) {
62
296
  const filePath = path.join(dir, `${baseName}-${counter}${extension}`);
63
- if (!fs5.existsSync(filePath)) {
297
+ if (!fs.existsSync(filePath)) {
64
298
  return filePath;
65
299
  }
66
300
  counter++;
67
301
  }
68
302
  }
303
+ function getAvailableFilePath(dir, baseName, extension) {
304
+ const firstFilePath = path.join(dir, `${baseName}${extension}`);
305
+ if (!fs.existsSync(firstFilePath)) {
306
+ return firstFilePath;
307
+ }
308
+ return getNextFilePath(dir, baseName, extension);
309
+ }
69
310
  function deleteFile(filePath) {
70
- if (fs5.existsSync(filePath)) {
71
- fs5.unlinkSync(filePath);
311
+ if (fs.existsSync(filePath)) {
312
+ fs.unlinkSync(filePath);
72
313
  }
73
314
  }
74
315
  function deleteTempDiff() {
75
316
  deleteFile(tempDiffPath);
76
317
  }
77
318
  function createReportDirectory() {
78
- if (!fs5.existsSync(REPORT_DIR)) {
79
- fs5.mkdirSync(REPORT_DIR, { recursive: true });
80
- }
81
- }
82
- function getNowString() {
83
- const now = /* @__PURE__ */ new Date();
84
- const YYYY = now.getFullYear();
85
- const MM = String(now.getMonth() + 1).padStart(2, "0");
86
- const DD = String(now.getDate()).padStart(2, "0");
87
- const HH = String(now.getHours()).padStart(2, "0");
88
- const mm = String(now.getMinutes()).padStart(2, "0");
89
- const ss = String(now.getSeconds()).padStart(2, "0");
319
+ if (!fs.existsSync(REPORT_DIR)) {
320
+ fs.mkdirSync(REPORT_DIR, { recursive: true });
321
+ }
322
+ }
323
+ function getNowString(now = /* @__PURE__ */ new Date()) {
324
+ const { YYYY, MM, DD, HH, mm, ss } = getTimestampParts(now);
90
325
  return `${YYYY}-${MM}-${DD}_${HH}-${mm}-${ss}`;
91
326
  }
92
- function getGitDiffFilter() {
93
- const includeExtensions = ["*.ts", "*.tsx", "*.js", "*.jsx"];
94
- const excludePatterns = ignoreList.map((item) => `:(exclude)${item}`);
95
- const quote = (pattern) => `"${pattern}"`;
96
- const includeParams = includeExtensions.map(quote).join(" ");
97
- const excludeParams = excludePatterns.map(quote).join(" ");
98
- return { includeParams, excludeParams };
327
+ function getErrorLogTimestamp(now = /* @__PURE__ */ new Date()) {
328
+ const { YYYY, MM, DD, HH, mm, ss } = getTimestampParts(now);
329
+ return `${YYYY}-${MM}-${DD}-${HH}\uC2DC-${mm}\uBD84-${ss}\uCD08`;
330
+ }
331
+ function writeErrorReport(error, options = {}) {
332
+ try {
333
+ const now = /* @__PURE__ */ new Date();
334
+ helperTrace("error-report:write:start", options.scope || "unknown");
335
+ createReportDirectory();
336
+ const reportPath = getAvailableFilePath(REPORT_DIR, `error-log-${getErrorLogTimestamp(now)}`, ".md");
337
+ const serializedError = serializeError(error);
338
+ const traceSnapshot = options.traceMessages ?? getTraceMessages();
339
+ const extraSections = options.extraSections || [];
340
+ const report = `# Error Log
341
+
342
+ - \uBC1C\uC0DD \uC2DC\uAC01: ${getHumanReadableNowString(now)}
343
+ - Scope: \`${options.scope || "unknown"}\`
344
+ - \uC791\uC5C5 \uACBD\uB85C: \`${process.cwd()}\`
345
+ - \uC2E4\uD589 \uC778\uC790: \`${JSON.stringify(options.args ?? process.argv.slice(2))}\`
346
+ - \uC2E4\uD589 \uD658\uACBD: \`${process.platform} ${process.arch} / Node ${process.version}\`
347
+
348
+ ## Summary
349
+
350
+ ${options.title || serializedError.summary || "Unknown error"}
351
+
352
+ ## Error
353
+
354
+ \`\`\`json
355
+ ${JSON.stringify(serializedError, null, 2)}
356
+ \`\`\`
357
+
358
+ ## Trace
359
+
360
+ \`\`\`json
361
+ ${JSON.stringify(traceSnapshot, null, 2)}
362
+ \`\`\`${extraSections.length ? `
363
+ ${extraSections.map((section) => `
364
+ ## ${section.heading}
365
+
366
+ ${section.markdown}`).join("\n")}
367
+ ` : "\n"}
368
+ `;
369
+ fs.writeFileSync(reportPath, report);
370
+ helperTrace("error-report:write:done", reportPath);
371
+ return reportPath;
372
+ } catch (writeError) {
373
+ console.error("\u26A0\uFE0F \uC5D0\uB7EC \uB85C\uADF8 \uD30C\uC77C \uC0DD\uC131\uC5D0 \uC2E4\uD328\uD588\uC2B5\uB2C8\uB2E4.");
374
+ console.error(writeError);
375
+ return "";
376
+ }
377
+ }
378
+ function exitWithError(message, options = {}) {
379
+ const reportPath = writeErrorReport(options.error || new Error(message), {
380
+ ...options,
381
+ title: message
382
+ });
383
+ console.error(message);
384
+ if (options.error) {
385
+ console.error(options.error);
386
+ }
387
+ if (reportPath) {
388
+ console.error(`\u{1F4C4} \uC5D0\uB7EC \uB85C\uADF8 \uC800\uC7A5 \uC704\uCE58: ${reportPath}`);
389
+ }
390
+ process.exit(1);
391
+ }
392
+ function parseServiceFromArgs(args4 = process.argv.slice(2)) {
393
+ helperTrace("parse-service:start", `args=${JSON.stringify(args4)}`);
394
+ const serviceIndex = args4.indexOf("--service");
395
+ const rawService = serviceIndex !== -1 ? args4[serviceIndex + 1] : "";
396
+ if (!rawService) {
397
+ helperTrace("parse-service:empty");
398
+ return "";
399
+ }
400
+ const normalizedService = rawService.toLowerCase();
401
+ if (AIServices.includes(normalizedService)) {
402
+ helperTrace("parse-service:resolved", normalizedService);
403
+ return normalizedService;
404
+ }
405
+ helperTrace("parse-service:invalid", rawService);
406
+ exitWithError(
407
+ `\u274C \uC9C0\uC6D0\uD558\uC9C0 \uC54A\uB294 \uC11C\uBE44\uC2A4\uC785\uB2C8\uB2E4: ${rawService}. \uC0AC\uC6A9 \uAC00\uB2A5 \uAC12: ${AIServices.join(", ")} (\uC608: --service codex)`,
408
+ {
409
+ scope: "helper:parseServiceFromArgs",
410
+ args: args4,
411
+ extraSections: [
412
+ {
413
+ heading: "Allowed Services",
414
+ markdown: `\`\`\`json
415
+ ${JSON.stringify(AIServices, null, 2)}
416
+ \`\`\``
417
+ }
418
+ ]
419
+ }
420
+ );
421
+ }
422
+ function truncateCommitSubject(subject) {
423
+ if (subject.length <= 72) {
424
+ return subject;
425
+ }
426
+ return `${subject.slice(0, 69)}...`;
427
+ }
428
+ function getRecentCommitOptions() {
429
+ const output = runGitCommand(
430
+ ["log", `-${COMMIT_FETCH_LIMIT}`, "--date=relative", "--pretty=format:%h%x09%an%x09%ar%x09%s"],
431
+ { allowFailure: true }
432
+ );
433
+ if (!output) {
434
+ return [];
435
+ }
436
+ return output.split("\n").map((line) => {
437
+ const [hash = "", author = "", relativeDate = "", ...subjectParts] = line.split(" ");
438
+ const subject = subjectParts.join(" ").trim();
439
+ return {
440
+ author,
441
+ description: `${author} | ${relativeDate}`,
442
+ hash,
443
+ label: `${hash} | ${truncateCommitSubject(subject)}`,
444
+ relativeDate,
445
+ subject
446
+ };
447
+ });
448
+ }
449
+ function buildSelectedCommitSummary(commits) {
450
+ return commits.map((commit) => `- ${commit.hash} | ${commit.subject} | ${commit.author} | ${commit.relativeDate}`).join("\n");
451
+ }
452
+ function getReviewPathspecArgs() {
453
+ const { includePatterns, excludePatterns } = getGitDiffPathspecs();
454
+ return [...includePatterns, ...excludePatterns];
455
+ }
456
+ function buildSelectedCommitDiff(commits) {
457
+ const reviewPathspecArgs = getReviewPathspecArgs();
458
+ const sections = commits.map((commit) => {
459
+ const diff = runGitCommand(["show", "--stat", "--patch", "--format=", commit.hash, "--", ...reviewPathspecArgs], {
460
+ allowFailure: true,
461
+ trimOutput: false
462
+ }).trim();
463
+ if (!diff) {
464
+ return "";
465
+ }
466
+ return [`## ${commit.hash} ${commit.subject}`, diff].join("\n\n");
467
+ }).filter(Boolean).join("\n\n");
468
+ if (!sections) {
469
+ return "";
470
+ }
471
+ return ["# \uC120\uD0DD\uD55C \uCEE4\uBC0B", buildSelectedCommitSummary(commits), "", "# \uB9AC\uBDF0 \uB300\uC0C1 diff", sections].join("\n");
472
+ }
473
+ function getSelectedCommitFiles(commits) {
474
+ const reviewPathspecArgs = getReviewPathspecArgs();
475
+ const files = /* @__PURE__ */ new Set();
476
+ commits.forEach((commit) => {
477
+ const output = runGitCommand(["show", "--pretty=format:", "--name-only", commit.hash, "--", ...reviewPathspecArgs], {
478
+ allowFailure: true
479
+ });
480
+ output.split("\n").map((line) => line.trim()).filter(Boolean).forEach((filePath) => files.add(filePath));
481
+ });
482
+ return [...files];
483
+ }
484
+ function buildSelectedFileDiff(commits, filePath) {
485
+ const sections = commits.map((commit) => {
486
+ const diff = runGitCommand(["show", "--stat", "--patch", "--format=", commit.hash, "--", filePath], {
487
+ allowFailure: true,
488
+ trimOutput: false
489
+ }).trim();
490
+ if (!diff) {
491
+ return "";
492
+ }
493
+ return [`## ${commit.hash} ${commit.subject}`, diff].join("\n\n");
494
+ }).filter(Boolean).join("\n\n");
495
+ if (!sections) {
496
+ return "";
497
+ }
498
+ return ["# \uC120\uD0DD\uD55C \uCEE4\uBC0B", buildSelectedCommitSummary(commits), "", `# \uD30C\uC77C: ${filePath}`, sections].join("\n\n");
99
499
  }
100
500
  function openReport(reportPath) {
101
501
  const resolvedPath = path.resolve(reportPath);
102
502
  const { platform } = process;
503
+ helperTrace("open-report:start", resolvedPath);
103
504
  const openWithChrome = () => {
104
505
  if (platform === "darwin") {
105
506
  execSync(`open -a "Google Chrome" "${resolvedPath}"`, { stdio: "ignore" });
@@ -124,63 +525,188 @@ function openReport(reportPath) {
124
525
  };
125
526
  try {
126
527
  if (openWithChrome()) {
528
+ helperTrace("open-report:chrome:success", platform);
127
529
  console.log("\u{1F680} Google Chrome\uC5D0\uC11C \uB9AC\uD3EC\uD2B8\uB97C \uC5F4\uC5C8\uC2B5\uB2C8\uB2E4.");
128
530
  return;
129
531
  }
130
- } catch {
532
+ } catch (error) {
533
+ helperTrace("open-report:chrome:failed", getErrorSummary(error));
131
534
  }
132
535
  try {
133
536
  if (openWithDefaultBrowser()) {
537
+ helperTrace("open-report:default-browser:success", platform);
134
538
  console.log("\u{1F680} \uAE30\uBCF8 \uBE0C\uB77C\uC6B0\uC800\uC5D0\uC11C \uB9AC\uD3EC\uD2B8\uB97C \uC5F4\uC5C8\uC2B5\uB2C8\uB2E4.");
135
539
  return;
136
540
  }
137
- } catch (e) {
138
- console.error("\u26A0\uFE0F \uBE0C\uB77C\uC6B0\uC800 \uC5F4\uAE30 \uC2E4\uD328:", e);
541
+ } catch (error) {
542
+ helperTrace("open-report:default-browser:failed", getErrorSummary(error));
543
+ console.error("\u26A0\uFE0F \uBE0C\uB77C\uC6B0\uC800 \uC5F4\uAE30 \uC2E4\uD328:", error);
139
544
  return;
140
545
  }
546
+ helperTrace("open-report:unsupported-platform", platform);
141
547
  console.error(`\u26A0\uFE0F \uC9C0\uC6D0\uD558\uC9C0 \uC54A\uB294 \uD50C\uB7AB\uD3FC\uC785\uB2C8\uB2E4: ${platform}`);
142
548
  }
143
- function getDiffArgs() {
144
- const args4 = process.argv.slice(2);
145
- const commitIndex = args4.indexOf("--commit");
146
- const { includeParams, excludeParams } = getGitDiffFilter();
147
- let diffArgs = "";
148
- if (commitIndex !== -1) {
149
- const commitHash = args4[commitIndex + 1];
150
- if (!commitHash) {
151
- console.error("\u274C \uCEE4\uBC0B \uD574\uC2DC\uAC00 \uC81C\uACF5\uB418\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4.");
152
- process.exit(1);
549
+ function ensureInteractiveSelectionAvailable(scope, message) {
550
+ if (process.stdin.isTTY && process.stdout.isTTY && typeof process.stdin.setRawMode === "function") {
551
+ return;
552
+ }
553
+ helperTrace(`${scope}:tty-missing`);
554
+ exitWithError(message, {
555
+ scope: `helper:${scope}`
556
+ });
557
+ }
558
+ function renderSelectionBlock(lines, previousLineCount) {
559
+ if (previousLineCount > 0) {
560
+ readline.moveCursor(process.stdout, 0, -previousLineCount);
561
+ readline.clearScreenDown(process.stdout);
562
+ }
563
+ const fittedLines = fitLinesToTerminal(lines);
564
+ process.stdout.write(`${fittedLines.join("\n")}
565
+ `);
566
+ return fittedLines.length;
567
+ }
568
+ function getSelectionWindowRange(optionCount, selectedIndex, windowSize) {
569
+ if (optionCount <= windowSize) {
570
+ return {
571
+ end: optionCount,
572
+ start: 0
573
+ };
574
+ }
575
+ const halfWindow = Math.floor(windowSize / 2);
576
+ const maxStart = optionCount - windowSize;
577
+ const start = Math.max(0, Math.min(selectedIndex - halfWindow, maxStart));
578
+ return {
579
+ end: Math.min(optionCount, start + windowSize),
580
+ start
581
+ };
582
+ }
583
+ function buildMultiSelectLines(question, options, selectedIndex, toggled, windowSize) {
584
+ const { start, end } = getSelectionWindowRange(options.length, selectedIndex, windowSize);
585
+ const lines = [
586
+ `${ANSI.bold}${question}${ANSI.reset}`,
587
+ `${ANSI.dim}\u2191\u2193 \uC774\uB3D9 | Space \uC120\uD0DD/\uD574\uC81C | Enter \uC644\uB8CC | Esc \uCDE8\uC18C${ANSI.reset}`,
588
+ `${ANSI.dim}\uC120\uD0DD\uB428: ${toggled.size}\uAC1C / \uC804\uCCB4: ${options.length}\uAC1C${ANSI.reset}`
589
+ ];
590
+ for (let index = start; index < end; index += 1) {
591
+ const option = options[index];
592
+ if (!option) {
593
+ continue;
153
594
  }
154
- const nextArg = args4[commitIndex + 2];
155
- let n = 0;
156
- if (nextArg && !nextArg.startsWith("--")) {
157
- n = parseInt(nextArg, 10);
158
- if (isNaN(n)) {
159
- n = 0;
160
- }
595
+ const cursor = index === selectedIndex ? `${ANSI.cyan}>${ANSI.reset}` : " ";
596
+ const checked = toggled.has(index) ? `${ANSI.green}\u2611${ANSI.reset}` : "\u2610";
597
+ const description = option.description ? ` ${ANSI.dim}${option.description}${ANSI.reset}` : "";
598
+ lines.push(`${cursor} ${checked} ${option.label}${description}`);
599
+ }
600
+ if (options.length > windowSize) {
601
+ lines.push(`${ANSI.dim}\uD45C\uC2DC \uBC94\uC704: ${start + 1}-${end} / ${options.length}${ANSI.reset}`);
602
+ }
603
+ return lines;
604
+ }
605
+ async function showMultiSelect(question, options, windowSize = COMMIT_SELECTION_WINDOW) {
606
+ ensureInteractiveSelectionAvailable("showMultiSelect", "\u274C \uCEE4\uBC0B \uC120\uD0DD \uBAA8\uB2EC\uC740 TTY \uD658\uACBD\uC5D0\uC11C\uB9CC \uC0AC\uC6A9\uD560 \uC218 \uC788\uC2B5\uB2C8\uB2E4.");
607
+ let selectedIndex = 0;
608
+ let renderedLineCount = 0;
609
+ const toggled = /* @__PURE__ */ new Set();
610
+ const rl = readline.createInterface({
611
+ input: process.stdin,
612
+ output: process.stdout,
613
+ terminal: true
614
+ });
615
+ process.stdout.write("\x1B[?25l");
616
+ const cleanup = () => {
617
+ if (renderedLineCount > 0) {
618
+ readline.moveCursor(process.stdout, 0, -renderedLineCount);
619
+ readline.clearScreenDown(process.stdout);
620
+ renderedLineCount = 0;
161
621
  }
162
- console.log(`\u2139\uFE0F \uCEE4\uBC0B '${commitHash}' ${n > 0 ? ` \uD3EC\uD568 \uCD1D ${n + 1}\uAC1C\uC758 \uCEE4\uBC0B` : ""}\uC744 \uB9AC\uBDF0\uD569\uB2C8\uB2E4...`);
163
- diffArgs = `${commitHash}~${n + 1} ${commitHash}`;
164
- } else {
165
- try {
166
- const check = execSync(`git diff --name-only -- ${includeParams} ${excludeParams}`).toString();
167
- if (!check.trim()) {
168
- 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...");
169
- diffArgs = "HEAD~1 HEAD";
622
+ process.stdin.removeListener("data", onData);
623
+ process.stdin.setRawMode(false);
624
+ process.stdin.pause();
625
+ rl.close();
626
+ process.stdout.write("\x1B[?25h");
627
+ };
628
+ const render = () => {
629
+ const lines = buildMultiSelectLines(question, options, selectedIndex, toggled, windowSize);
630
+ renderedLineCount = renderSelectionBlock(lines, renderedLineCount);
631
+ };
632
+ const confirmSelection = (resolve) => {
633
+ const values = [...toggled].sort((left, right) => left - right).map((index) => options[index]?.value).filter((value) => value !== void 0);
634
+ cleanup();
635
+ resolve(values);
636
+ };
637
+ const cancelSelection = (resolve) => {
638
+ cleanup();
639
+ resolve([]);
640
+ };
641
+ let onData = (_data) => {
642
+ };
643
+ render();
644
+ return new Promise((resolve) => {
645
+ onData = (data) => {
646
+ const key = data.toString();
647
+ if (key === "") {
648
+ cleanup();
649
+ process.exit(0);
170
650
  }
171
- } catch {
172
- }
651
+ if (key === "\x1B") {
652
+ cancelSelection(resolve);
653
+ return;
654
+ }
655
+ if (key === "\x1B[A") {
656
+ selectedIndex = (selectedIndex - 1 + options.length) % options.length;
657
+ render();
658
+ return;
659
+ }
660
+ if (key === "\x1B[B") {
661
+ selectedIndex = (selectedIndex + 1) % options.length;
662
+ render();
663
+ return;
664
+ }
665
+ if (key === " ") {
666
+ if (toggled.has(selectedIndex)) {
667
+ toggled.delete(selectedIndex);
668
+ } else {
669
+ toggled.add(selectedIndex);
670
+ }
671
+ render();
672
+ return;
673
+ }
674
+ if (key === "\r" || key === "\n") {
675
+ confirmSelection(resolve);
676
+ }
677
+ };
678
+ process.stdin.setRawMode(true);
679
+ process.stdin.resume();
680
+ process.stdin.on("data", onData);
681
+ });
682
+ }
683
+ async function selectReviewCommits() {
684
+ const commits = getRecentCommitOptions();
685
+ if (commits.length === 0) {
686
+ console.log("\u2139\uFE0F \uB9AC\uBDF0\uD560 \uCD5C\uADFC \uCEE4\uBC0B\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.");
687
+ return [];
173
688
  }
174
- return diffArgs;
689
+ return showMultiSelect(
690
+ "\uB9AC\uBDF0\uD560 \uCEE4\uBC0B\uC744 \uC120\uD0DD\uD574\uC8FC\uC138\uC694.",
691
+ commits.map((commit) => ({
692
+ description: commit.description,
693
+ label: commit.label,
694
+ value: commit
695
+ })),
696
+ COMMIT_SELECTION_WINDOW
697
+ );
175
698
  }
176
699
  async function showSelectionAIService() {
177
700
  const selectedServiceFromArgs = parseServiceFromArgs();
178
701
  if (selectedServiceFromArgs) {
702
+ helperTrace("show-selection:from-args", selectedServiceFromArgs);
179
703
  console.log(`
180
- \u2705 \x1B[32m${selectedServiceFromArgs}\x1B[0m \uC11C\uBE44\uC2A4\uAC00 \uC120\uD0DD\uB418\uC5C8\uC2B5\uB2C8\uB2E4. (--service)
704
+ \u2705 ${ANSI.green}${selectedServiceFromArgs}${ANSI.reset} \uC11C\uBE44\uC2A4\uAC00 \uC120\uD0DD\uB418\uC5C8\uC2B5\uB2C8\uB2E4. (--service)
181
705
  `);
182
706
  return selectedServiceFromArgs;
183
707
  }
708
+ 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.");
709
+ helperTrace("show-selection:interactive:start");
184
710
  let selectedIndex = 0;
185
711
  const rl = readline.createInterface({
186
712
  input: process.stdin,
@@ -194,13 +720,15 @@ async function showSelectionAIService() {
194
720
  readline.moveCursor(process.stdout, 0, -(AIServices.length + 1));
195
721
  }
196
722
  firstRender = false;
723
+ helperTrace("show-selection:interactive:render", AIServices[selectedIndex] || "unknown");
197
724
  readline.clearScreenDown(process.stdout);
198
725
  process.stdout.write(
199
- "\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"
726
+ `\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):
727
+ `
200
728
  );
201
729
  AIServices.forEach((service, index) => {
202
730
  if (index === selectedIndex) {
203
- process.stdout.write(` \x1B[36m>\x1B[0m \x1B[36m\u25C9\x1B[0m \x1B[1m${service}\x1B[0m
731
+ process.stdout.write(` ${ANSI.cyan}>${ANSI.reset} ${ANSI.cyan}\u25C9${ANSI.reset} ${ANSI.bold}${service}${ANSI.reset}
204
732
  `);
205
733
  } else {
206
734
  process.stdout.write(` \u25EF ${service}
@@ -213,6 +741,7 @@ async function showSelectionAIService() {
213
741
  const onData = (data) => {
214
742
  const key = data.toString();
215
743
  if (key === "") {
744
+ helperTrace("show-selection:interactive:ctrl-c");
216
745
  process.stdout.write("\x1B[?25h");
217
746
  process.exit(0);
218
747
  }
@@ -229,10 +758,11 @@ async function showSelectionAIService() {
229
758
  rl.close();
230
759
  process.stdout.write("\x1B[?25h");
231
760
  console.log(`
232
- \u2705 \x1B[32m${AIServices[selectedIndex]}\x1B[0m \uC11C\uBE44\uC2A4\uAC00 \uC120\uD0DD\uB418\uC5C8\uC2B5\uB2C8\uB2E4.
761
+ \u2705 ${ANSI.green}${AIServices[selectedIndex]}${ANSI.reset} \uC11C\uBE44\uC2A4\uAC00 \uC120\uD0DD\uB418\uC5C8\uC2B5\uB2C8\uB2E4.
233
762
  `);
234
763
  const result = AIServices[selectedIndex];
235
764
  if (result) {
765
+ helperTrace("show-selection:interactive:confirmed", result);
236
766
  resolve(result);
237
767
  }
238
768
  }
@@ -255,6 +785,11 @@ function getArgValue(flag) {
255
785
  }
256
786
  return args[index + 1];
257
787
  }
788
+ function printNotice(message) {
789
+ if (args.includes("--test")) {
790
+ console.warn(message);
791
+ }
792
+ }
258
793
  function toUnique(values) {
259
794
  const seen = /* @__PURE__ */ new Set();
260
795
  return values.filter((value) => {
@@ -278,11 +813,11 @@ function resolveReasoningEffort() {
278
813
  const normalized = normalizeEffort(customReasoningEffort);
279
814
  trace("reasoning:custom", `${customReasoningEffort} -> ${normalized}`);
280
815
  if (customReasoningEffort === "minimal") {
281
- console.warn("\u26A0\uFE0F Claude\uB294 minimal\uC744 \uC9C1\uC811 \uC9C0\uC6D0\uD558\uC9C0 \uC54A\uC544 low\uB85C \uB9E4\uD551\uD569\uB2C8\uB2E4.");
816
+ printNotice("\u26A0\uFE0F Claude\uB294 minimal\uC744 \uC9C1\uC811 \uC9C0\uC6D0\uD558\uC9C0 \uC54A\uC544 low\uB85C \uB9E4\uD551\uD569\uB2C8\uB2E4.");
282
817
  }
283
818
  return normalized;
284
819
  }
285
- console.warn(
820
+ printNotice(
286
821
  `\u26A0\uFE0F \uC9C0\uC6D0\uB418\uC9C0 \uC54A\uB294 reasoning effort(${customReasoningEffort})\uC785\uB2C8\uB2E4. allowed: ${ALLOWED_REASONING_EFFORTS.join(
287
822
  ", "
288
823
  )}`
@@ -325,7 +860,7 @@ function buildClaudeExecCommand(options) {
325
860
  const modelOption = model ? `--model ${shellQuote(model)}` : "";
326
861
  const fallbackOption = model && fallbackModel ? `--fallback-model ${shellQuote(fallbackModel)}` : "";
327
862
  const effortOption = `--effort ${shellQuote(effort)}`;
328
- const appendedPromptFiles = systemPromptFiles.map((path2) => `--append-system-prompt-file ${shellQuote(path2)}`).join(" ");
863
+ const appendedPromptFiles = systemPromptFiles.map((path3) => `--append-system-prompt-file ${shellQuote(path3)}`).join(" ");
329
864
  return `cat ${shellQuote(tempDiffPath2)} | claude ${[
330
865
  modelOption,
331
866
  fallbackOption,
@@ -346,22 +881,25 @@ var createClaudeCommand = (tempDiffPath2, reviewFormPath2) => {
346
881
  { path: namingRulesPath, display: "\uB124\uC774\uBC0D \uADDC\uCE59" },
347
882
  { path: codingConventionRulesPath, display: "\uCF54\uB529 \uCEE8\uBCA4\uC158" }
348
883
  ];
349
- const existingRuleFiles = rules.filter((rule) => fs5.existsSync(rule.path)).map((rule) => rule.path);
884
+ const existingRuleFiles = rules.filter((rule) => fs.existsSync(rule.path)).map((rule) => rule.path);
350
885
  trace("rules:loaded", `count=${existingRuleFiles.length}`);
351
- const reviewFormExists = fs5.existsSync(reviewFormPath2);
886
+ const reviewFormExists = fs.existsSync(reviewFormPath2);
352
887
  trace("reviewForm:status", reviewFormExists ? "exists" : "missing");
353
888
  const systemPromptFiles = reviewFormExists ? [...existingRuleFiles, reviewFormPath2] : existingRuleFiles;
354
889
  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.";
890
+ trace("prompt:prepared", `length=${prompt.length}`);
891
+ trace("system-prompt-files", `count=${systemPromptFiles.length}`);
355
892
  const modelCandidates = toUnique(customModel ? [customModel, ...aliasFallbacks] : aliasFallbacks);
356
893
  trace("model:candidates", modelCandidates.join(", "));
894
+ trace("command:candidates:count", String(modelCandidates.length + 1));
357
895
  if (customModel) {
358
- console.warn(
896
+ printNotice(
359
897
  `\u26A0\uFE0F \uCEE4\uC2A4\uD140 \uBAA8\uB378(${customModel})\uC744 \uC6B0\uC120 \uC2DC\uB3C4\uD558\uACE0 \uC2E4\uD328\uD558\uBA74 alias(${aliasFallbacks.join(
360
898
  " -> "
361
899
  )}) \uBC0F \uAE30\uBCF8 \uBAA8\uB378 \uC21C\uC73C\uB85C \uD3F4\uBC31\uD569\uB2C8\uB2E4.`
362
900
  );
363
901
  } else {
364
- console.warn(
902
+ printNotice(
365
903
  `\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.`
366
904
  );
367
905
  }
@@ -394,6 +932,7 @@ var createClaudeCommand = (tempDiffPath2, reviewFormPath2) => {
394
932
  \uC0DD\uC131\uB420 \uBA85\uB839\uC5B4 \uBBF8\uB9AC\uBCF4\uAE30:
395
933
  ${safeCommand}"`;
396
934
  }
935
+ trace("command:mode", "execute");
397
936
  trace("createClaudeCommand:end");
398
937
  return command;
399
938
  };
@@ -404,23 +943,25 @@ function checkClaudeCliInstalled() {
404
943
  trace2("version-check:run", "claude --version");
405
944
  execSync("claude --version", { stdio: "ignore" });
406
945
  trace2("version-check:ok");
407
- } catch {
408
- trace2("version-check:failed", "install-start");
946
+ } catch (error) {
947
+ trace2("version-check:failed", getErrorSummary(error));
948
+ trace2("install:start", "@anthropic-ai/claude-code");
409
949
  console.log(
410
950
  "\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"
411
951
  );
412
952
  try {
413
953
  execSync("npm install -g @anthropic-ai/claude-code", { stdio: "inherit" });
414
- trace2("install:ok", "exit(1) for login");
954
+ trace2("install:ok", "login-required");
415
955
  console.log("\u2705 claude-cli \uC124\uCE58\uAC00 \uC644\uB8CC\uB418\uC5C8\uC2B5\uB2C8\uB2E4.");
416
956
  console.log("\u26A0\uFE0F claude-cli \uC0AC\uC6A9\uC744 \uC704\uD574 \uC778\uC99D\uC774 \uD544\uC694\uD569\uB2C8\uB2E4.");
417
957
  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.');
418
958
  process.exit(1);
419
959
  } catch (installError) {
420
- trace2("install:failed");
421
- 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).");
422
- console.error(installError);
423
- process.exit(1);
960
+ trace2("install:failed", getErrorSummary(installError));
961
+ 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).", {
962
+ scope: "installation-claude",
963
+ error: installError
964
+ });
424
965
  }
425
966
  }
426
967
  trace2("checkClaudeCliInstalled:end");
@@ -438,6 +979,11 @@ function getArgValue2(flag) {
438
979
  }
439
980
  return args2[index + 1];
440
981
  }
982
+ function printNotice2(message) {
983
+ if (args2.includes("--test")) {
984
+ console.warn(message);
985
+ }
986
+ }
441
987
  function resolveReasoningEffort2() {
442
988
  const customReasoningEffort = getArgValue2("--reasoning-effort");
443
989
  if (customReasoningEffort) {
@@ -445,7 +991,7 @@ function resolveReasoningEffort2() {
445
991
  trace3("reasoning:custom", customReasoningEffort);
446
992
  return customReasoningEffort;
447
993
  }
448
- console.warn(
994
+ printNotice2(
449
995
  `\u26A0\uFE0F \uC9C0\uC6D0\uB418\uC9C0 \uC54A\uB294 reasoning effort(${customReasoningEffort})\uC785\uB2C8\uB2E4. allowed: ${ALLOWED_REASONING_EFFORTS2.join(
450
996
  ", "
451
997
  )}`
@@ -471,10 +1017,10 @@ var createCodexCommand = (tempDiffPath2, reviewFormPath2) => {
471
1017
  trace3("createCodexCommand:start", `tempDiffPath=${tempDiffPath2}, reviewFormPath=${reviewFormPath2}`);
472
1018
  const customModel = getArgValue2("--model");
473
1019
  const reasoningEffort = resolveReasoningEffort2();
474
- const rules = [rulesPath, namingRulesPath, codingConventionRulesPath].filter((filePath) => fs5.existsSync(filePath)).map((filePath) => `- ${filePath}`).join("\n");
1020
+ const rules = [rulesPath, namingRulesPath, codingConventionRulesPath].filter((filePath) => fs.existsSync(filePath)).map((filePath) => `- ${filePath}`).join("\n");
475
1021
  const rulesCount = rules ? rules.split("\n").length : 0;
476
1022
  trace3("rules:loaded", `count=${rulesCount}`);
477
- const hasReviewForm = fs5.existsSync(reviewFormPath2);
1023
+ const hasReviewForm = fs.existsSync(reviewFormPath2);
478
1024
  const reviewFormLine = hasReviewForm ? `- ${reviewFormPath2}` : "";
479
1025
  trace3("reviewForm:status", reviewFormLine ? "exists" : "missing");
480
1026
  const prompt = `\uC544\uB798 \uD30C\uC77C\uB4E4\uC744 \uCC38\uACE0\uD574\uC11C \uCF54\uB4DC \uB9AC\uBDF0\uB97C \uC9C4\uD589\uD574\uC918.
@@ -486,16 +1032,17 @@ ${reviewFormLine || "- (\uC5C6\uC74C)"}
486
1032
  - ${tempDiffPath2}
487
1033
 
488
1034
  \uBC18\uB4DC\uC2DC \uB9AC\uBDF0 \uC591\uC2DD\uC5D0 \uB9DE\uCDB0 \uC791\uC131\uD574\uC918.`;
1035
+ trace3("prompt:prepared", `length=${prompt.length}`);
489
1036
  let command = "";
490
1037
  if (customModel) {
491
- 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.");
1038
+ 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.");
492
1039
  trace3("model:custom", customModel);
493
1040
  command = buildCodexExecCommand(prompt, reasoningEffort, customModel);
494
1041
  } else {
495
1042
  const preferredModelAlias = "gpt-5";
496
1043
  const aliasCommand = buildCodexExecCommand(prompt, reasoningEffort, preferredModelAlias);
497
1044
  const fallbackCommand = buildCodexExecCommand(prompt, reasoningEffort);
498
- console.warn(
1045
+ printNotice2(
499
1046
  `\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.`
500
1047
  );
501
1048
  trace3("model:alias-first", preferredModelAlias);
@@ -511,6 +1058,7 @@ ${reviewFormLine || "- (\uC5C6\uC74C)"}
511
1058
  \uC0DD\uC131\uB420 \uBA85\uB839\uC5B4 \uBBF8\uB9AC\uBCF4\uAE30:
512
1059
  ${safeCommand}"`;
513
1060
  }
1061
+ trace3("command:mode", "execute");
514
1062
  trace3("createCodexCommand:end");
515
1063
  return command;
516
1064
  };
@@ -521,21 +1069,23 @@ function checkCodexCliInstalled() {
521
1069
  trace4("version-check:run", "codex --version");
522
1070
  execSync("codex --version", { stdio: "ignore" });
523
1071
  trace4("version-check:ok");
524
- } catch {
525
- trace4("version-check:failed", "install-start");
1072
+ } catch (error) {
1073
+ trace4("version-check:failed", getErrorSummary(error));
1074
+ trace4("install:start", "@openai/codex");
526
1075
  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");
527
1076
  try {
528
1077
  execSync("npm install -g @openai/codex", { stdio: "inherit" });
529
- trace4("install:ok", "exit(1) for login");
1078
+ trace4("install:ok", "login-required");
530
1079
  console.log("\u2705 codex-cli \uC124\uCE58\uAC00 \uC644\uB8CC\uB418\uC5C8\uC2B5\uB2C8\uB2E4.");
531
1080
  console.log("\u26A0\uFE0F codex-cli \uC0AC\uC6A9\uC744 \uC704\uD574 \uC778\uC99D\uC774 \uD544\uC694\uD569\uB2C8\uB2E4.");
532
1081
  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.');
533
1082
  process.exit(1);
534
1083
  } catch (installError) {
535
- trace4("install:failed");
536
- 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).");
537
- console.error(installError);
538
- process.exit(1);
1084
+ trace4("install:failed", getErrorSummary(installError));
1085
+ 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).", {
1086
+ scope: "installation-codex",
1087
+ error: installError
1088
+ });
539
1089
  }
540
1090
  }
541
1091
  trace4("checkCodexCliInstalled:end");
@@ -553,6 +1103,11 @@ function getArgValue3(flag) {
553
1103
  }
554
1104
  return args3[index + 1];
555
1105
  }
1106
+ function printNotice3(message) {
1107
+ if (args3.includes("--test")) {
1108
+ console.warn(message);
1109
+ }
1110
+ }
556
1111
  function toUnique2(values) {
557
1112
  const seen = /* @__PURE__ */ new Set();
558
1113
  return values.filter((value) => {
@@ -570,7 +1125,7 @@ function resolveReasoningEffort3() {
570
1125
  trace5("reasoning:custom", customReasoningEffort);
571
1126
  return customReasoningEffort;
572
1127
  }
573
- console.warn(
1128
+ printNotice3(
574
1129
  `\u26A0\uFE0F \uC9C0\uC6D0\uB418\uC9C0 \uC54A\uB294 reasoning effort(${customReasoningEffort})\uC785\uB2C8\uB2E4. allowed: ${ALLOWED_REASONING_EFFORTS3.join(
575
1130
  ", "
576
1131
  )}`
@@ -632,36 +1187,57 @@ function buildGeminiExecCommand(prompt, model) {
632
1187
  const modelOption = model ? `--model ${shellQuote3(model)}` : "";
633
1188
  return `gemini ${[modelOption, "-p", shellQuote3(prompt)].filter(Boolean).join(" ")}`;
634
1189
  }
1190
+ function toGeminiFileReference(filePath) {
1191
+ return `@${filePath}`;
1192
+ }
1193
+ function buildGeminiFileReferenceSection(files) {
1194
+ const existingFiles = files.filter((file) => fs.existsSync(file.path));
1195
+ return {
1196
+ count: existingFiles.length,
1197
+ lines: existingFiles.map((file) => `- ${file.display}: ${toGeminiFileReference(file.path)}`)
1198
+ };
1199
+ }
635
1200
  var createGeminiCommand = (tempDiffPath2, reviewFormPath2) => {
636
1201
  trace5("createGeminiCommand:start", `tempDiffPath=${tempDiffPath2}, reviewFormPath=${reviewFormPath2}`);
637
1202
  const customModel = getArgValue3("--model");
638
1203
  const reasoningEffort = resolveReasoningEffort3();
639
1204
  const primaryAlias = resolvePrimaryAlias2(reasoningEffort);
640
1205
  const aliasFallbacks = toUnique2(getAliasFallbacks2(primaryAlias));
1206
+ const resolvedTempDiffPath = path.resolve(tempDiffPath2);
1207
+ const resolvedReviewFormPath = path.resolve(reviewFormPath2);
641
1208
  const rules = [
642
1209
  { path: rulesPath, display: "\uB8F0\uC14B" },
643
1210
  { path: namingRulesPath, display: "\uB124\uC774\uBC0D \uADDC\uCE59" },
644
1211
  { path: codingConventionRulesPath, display: "\uCF54\uB529 \uCEE8\uBCA4\uC158" }
645
1212
  ];
646
- const validRules = rules.filter((rule) => fs5.existsSync(rule.path)).map((rule) => `@${rule.path}`).join(", ");
647
- const rulesCount = validRules ? validRules.split(",").length : 0;
648
- trace5("rules:loaded", `count=${rulesCount}`);
649
- const reviewFormRef = fs5.existsSync(reviewFormPath2) ? `@${reviewFormPath2}` : "(\uC5C6\uC74C)";
650
- trace5("reviewForm:status", reviewFormRef === "(\uC5C6\uC74C)" ? "missing" : "exists");
1213
+ const ruleSection = buildGeminiFileReferenceSection(rules);
1214
+ trace5("rules:loaded", `count=${ruleSection.count}`);
1215
+ const reviewFormExists = fs.existsSync(resolvedReviewFormPath);
1216
+ const reviewFormLine = reviewFormExists ? `- \uB9AC\uBDF0 \uC591\uC2DD: ${toGeminiFileReference(resolvedReviewFormPath)}` : "- \uB9AC\uBDF0 \uC591\uC2DD: (\uC5C6\uC74C)";
1217
+ trace5("reviewForm:status", reviewFormExists ? "exists" : "missing");
651
1218
  const reasoningInstruction = getReasoningInstruction(reasoningEffort);
652
- const prompt = `\uB2E4\uC74C \uADDC\uCE59\uB4E4\uC744 \uCC38\uACE0\uD574\uC11C(${validRules || "(\uC5C6\uC74C)"}) \uC774 diff(@${tempDiffPath2})\uB97C \uB9AC\uBDF0\uD574\uC918.
653
- \uB9AC\uBDF0 \uC591\uC2DD\uC740 ${reviewFormRef} \uC5D0 \uB9DE\uCDB0\uC11C \uC791\uC131\uD574\uC918.
1219
+ const prompt = `\uC544\uB798 \uD30C\uC77C\uB4E4\uC744 \uC21C\uC11C\uB300\uB85C \uC77D\uACE0 \uCF54\uB4DC \uB9AC\uBDF0\uB97C \uC9C4\uD589\uD574\uC918.
1220
+ \uADDC\uCE59 \uD30C\uC77C:
1221
+ ${ruleSection.lines.join("\n") || "- (\uC5C6\uC74C)"}
1222
+ \uB9AC\uBDF0 \uC591\uC2DD \uD30C\uC77C:
1223
+ ${reviewFormLine}
1224
+ \uB9AC\uBDF0 \uB300\uC0C1 diff \uD30C\uC77C:
1225
+ - \uB9AC\uBDF0 \uB300\uC0C1 diff: ${toGeminiFileReference(resolvedTempDiffPath)}
1226
+
1227
+ \uBC18\uB4DC\uC2DC \uB9AC\uBDF0 \uC591\uC2DD\uC5D0 \uB9DE\uCDB0 \uC791\uC131\uD574\uC918.
654
1228
  \uCD94\uB860 \uAC15\uB3C4 \uC9C0\uCE68: ${reasoningInstruction}`;
1229
+ trace5("prompt:prepared", `length=${prompt.length}`);
655
1230
  const modelCandidates = toUnique2(customModel ? [customModel, ...aliasFallbacks] : aliasFallbacks);
656
1231
  trace5("model:candidates", modelCandidates.join(", "));
1232
+ trace5("command:candidates:count", String(modelCandidates.length + 1));
657
1233
  if (customModel) {
658
- console.warn(
1234
+ printNotice3(
659
1235
  `\u26A0\uFE0F \uCEE4\uC2A4\uD140 \uBAA8\uB378(${customModel})\uC744 \uC6B0\uC120 \uC2DC\uB3C4\uD558\uACE0 \uC2E4\uD328\uD558\uBA74 alias(${aliasFallbacks.join(
660
1236
  " -> "
661
1237
  )}) \uBC0F \uAE30\uBCF8 \uBAA8\uB378 \uC21C\uC73C\uB85C \uD3F4\uBC31\uD569\uB2C8\uB2E4.`
662
1238
  );
663
1239
  } else {
664
- console.warn(
1240
+ printNotice3(
665
1241
  `\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.`
666
1242
  );
667
1243
  }
@@ -676,6 +1252,7 @@ var createGeminiCommand = (tempDiffPath2, reviewFormPath2) => {
676
1252
  \uC0DD\uC131\uB420 \uBA85\uB839\uC5B4 \uBBF8\uB9AC\uBCF4\uAE30:
677
1253
  ${safeCommand}"`;
678
1254
  }
1255
+ trace5("command:mode", "execute");
679
1256
  trace5("createGeminiCommand:end");
680
1257
  return command;
681
1258
  };
@@ -686,21 +1263,23 @@ function checkGeminiCliInstalled() {
686
1263
  trace6("version-check:run", "gemini --version");
687
1264
  execSync("gemini --version", { stdio: "ignore" });
688
1265
  trace6("version-check:ok");
689
- } catch {
690
- trace6("version-check:failed", "install-start");
1266
+ } catch (error) {
1267
+ trace6("version-check:failed", getErrorSummary(error));
1268
+ trace6("install:start", "@google/gemini-cli");
691
1269
  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");
692
1270
  try {
693
1271
  execSync("npm install -g @google/gemini-cli", { stdio: "inherit" });
694
- trace6("install:ok", "exit(1) for login");
1272
+ trace6("install:ok", "login-required");
695
1273
  console.log("\u2705 gemini-cli \uC124\uCE58\uAC00 \uC644\uB8CC\uB418\uC5C8\uC2B5\uB2C8\uB2E4.");
696
1274
  console.log("\u26A0\uFE0F Gemini API \uC0AC\uC6A9\uC744 \uC704\uD574 \uC778\uC99D\uC774 \uD544\uC694\uD569\uB2C8\uB2E4.");
697
1275
  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.');
698
1276
  process.exit(1);
699
1277
  } catch (installError) {
700
- trace6("install:failed");
701
- 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).");
702
- console.error(installError);
703
- process.exit(1);
1278
+ trace6("install:failed", getErrorSummary(installError));
1279
+ 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).", {
1280
+ scope: "installation-gemini",
1281
+ error: installError
1282
+ });
704
1283
  }
705
1284
  }
706
1285
  trace6("checkGeminiCliInstalled:end");
@@ -710,76 +1289,92 @@ function checkGeminiCliInstalled() {
710
1289
  var execAsync = util.promisify(exec);
711
1290
  async function main() {
712
1291
  const args4 = process.argv.slice(2);
1292
+ clearTraceMessages();
713
1293
  const isTest = isTestMode(args4);
714
1294
  const trace7 = createTraceLogger("review-one-by-one", args4);
715
1295
  trace7("main:start", `args=${JSON.stringify(args4)}`);
716
- trace7("service-selection:start");
717
- const service = await showSelectionAIService();
718
- trace7("service-selection:done", `service=${service}`);
719
- switch (service) {
720
- case "gemini":
721
- trace7("install-check:start", "service=gemini");
722
- checkGeminiCliInstalled();
723
- trace7("install-check:done", "service=gemini");
724
- break;
725
- case "claude":
726
- trace7("install-check:start", "service=claude");
727
- checkClaudeCliInstalled();
728
- trace7("install-check:done", "service=claude");
729
- break;
730
- case "codex":
731
- trace7("install-check:start", "service=codex");
732
- checkCodexCliInstalled();
733
- trace7("install-check:done", "service=codex");
734
- break;
735
- }
1296
+ let service = "";
1297
+ let savedDiffPath = "";
1298
+ let savedReportPath = "";
1299
+ let selectedCommitSummary = "";
736
1300
  try {
1301
+ trace7("service-selection:start");
1302
+ service = await showSelectionAIService();
1303
+ trace7("service-selection:done", `service=${service}`);
1304
+ switch (service) {
1305
+ case "gemini":
1306
+ trace7("install-check:start", "service=gemini");
1307
+ checkGeminiCliInstalled();
1308
+ trace7("install-check:done", "service=gemini");
1309
+ break;
1310
+ case "claude":
1311
+ trace7("install-check:start", "service=claude");
1312
+ checkClaudeCliInstalled();
1313
+ trace7("install-check:done", "service=claude");
1314
+ break;
1315
+ case "codex":
1316
+ trace7("install-check:start", "service=codex");
1317
+ checkCodexCliInstalled();
1318
+ trace7("install-check:done", "service=codex");
1319
+ break;
1320
+ }
737
1321
  trace7("review-flow:start");
738
1322
  console.log("\u{1F680} AI Code Review\uB97C \uC2DC\uC791\uD569\uB2C8\uB2E4...");
739
- trace7("report-dir:create:start");
740
- createReportDirectory();
741
- trace7("report-dir:create:done");
742
- const { includeParams, excludeParams } = getGitDiffFilter();
743
- trace7("diff-filter:loaded", `include=${includeParams} | exclude=${excludeParams}`);
744
- const diffArgs = getDiffArgs();
745
- trace7("diff-args:build:done", `diffArgs=${diffArgs || "(default)"}`);
746
- const filesCommand = `git diff --name-only ${diffArgs} -- ${includeParams} ${excludeParams}`;
747
- trace7("files-command:run", filesCommand);
748
- const fileList = execSync(filesCommand).toString().split("\n").filter(Boolean);
1323
+ trace7("commit-selection:start");
1324
+ const selectedCommits = await selectReviewCommits();
1325
+ trace7("commit-selection:done", `count=${selectedCommits.length}`);
1326
+ if (selectedCommits.length === 0) {
1327
+ trace7("commit-selection:empty");
1328
+ console.log("\u2139\uFE0F \uC120\uD0DD\uB41C \uCEE4\uBC0B\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.");
1329
+ deleteTempDiff();
1330
+ process.exit(0);
1331
+ }
1332
+ selectedCommitSummary = buildSelectedCommitSummary(selectedCommits);
1333
+ trace7("commit-summary:prepared", selectedCommitSummary);
1334
+ console.log("\u23F3 \uC120\uD0DD\uD55C \uCEE4\uBC0B\uC758 \uD30C\uC77C \uBAA9\uB85D\uACFC diff\uB97C \uC815\uB9AC\uD558\uB294 \uC911\uC785\uB2C8\uB2E4...");
1335
+ trace7("files-command:run");
1336
+ const fileList = getSelectedCommitFiles(selectedCommits);
749
1337
  trace7("files-command:done", `fileCount=${fileList.length}`);
1338
+ console.log(`\u{1F4C2} \uB9AC\uBDF0 \uB300\uC0C1 \uD30C\uC77C(${fileList.length}\uAC1C): ${formatReviewTargetFiles(fileList)}`);
750
1339
  if (fileList.length === 0) {
751
1340
  trace7("empty-file-list:exit");
752
- console.log("\u2139\uFE0F \uBCC0\uACBD \uC0AC\uD56D\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.");
1341
+ 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.");
753
1342
  deleteTempDiff();
754
1343
  process.exit(0);
755
1344
  }
756
- const fullDiffCommand = `git diff ${diffArgs} -- ${includeParams} ${excludeParams}`;
757
- trace7("full-diff:run");
758
- const fullDiff = execSync(fullDiffCommand).toString();
1345
+ trace7("report-dir:create:start");
1346
+ createReportDirectory();
1347
+ trace7("report-dir:create:done");
1348
+ trace7("full-diff:build:start");
1349
+ const fullDiff = buildSelectedCommitDiff(selectedCommits);
759
1350
  const nowStr = getNowString();
760
- trace7("full-diff:done", `length=${fullDiff.length}`);
1351
+ trace7("full-diff:build:done", `length=${fullDiff.length}`);
761
1352
  trace7("timestamp:created", nowStr);
762
1353
  trace7("temp-diff:write:start", tempDiffPath);
763
- fs5.writeFileSync(tempDiffPath, fullDiff);
1354
+ fs.writeFileSync(tempDiffPath, fullDiff);
764
1355
  trace7("temp-diff:write:done");
765
1356
  trace7("saved-diff:copy:start");
766
- const savedDiffPath = getNextFilePath(REPORT_DIR, `${nowStr}-diff`, ".txt");
767
- fs5.copyFileSync(tempDiffPath, savedDiffPath);
1357
+ savedDiffPath = getNextFilePath(REPORT_DIR, `${nowStr}-diff`, ".txt");
1358
+ fs.copyFileSync(tempDiffPath, savedDiffPath);
768
1359
  trace7("saved-diff:copy:done", savedDiffPath);
769
- const savedReportPath = getNextFilePath(REPORT_DIR, nowStr, ".md");
1360
+ savedReportPath = getNextFilePath(REPORT_DIR, nowStr, ".md");
770
1361
  trace7("saved-report:path", savedReportPath);
771
- const promises = fileList.map(async (file) => {
1362
+ const promises = fileList.map(async (file, index) => {
1363
+ const tempOneFileDiffPath = `temp_diff_${file.replace(/\//g, "_")}.txt`;
1364
+ let command = "";
772
1365
  try {
773
1366
  trace7("file-review:start", file);
774
- console.log(`\u{1F50D} Reviewing: ${file}...`);
775
- trace7("file-diff:run", file);
776
- const fileDiff = execSync(`git diff ${diffArgs} -- "${file}"`).toString();
777
- trace7("file-diff:done", `${file} | length=${fileDiff.length}`);
778
- const tempOneFileDiffPath = `temp_diff_${file.replace(/\//g, "_")}.txt`;
1367
+ console.log(`\u{1F50D} [${index + 1}/${fileList.length}] \uB9AC\uBDF0 \uC900\uBE44: ${file}`);
1368
+ trace7("file-diff:build:start", file);
1369
+ const fileDiff = buildSelectedFileDiff(selectedCommits, file);
1370
+ trace7("file-diff:build:done", `${file} | length=${fileDiff.length}`);
1371
+ if (!fileDiff.trim()) {
1372
+ trace7("file-diff:empty", file);
1373
+ return;
1374
+ }
779
1375
  trace7("file-temp-diff:write:start", tempOneFileDiffPath);
780
- fs5.writeFileSync(tempOneFileDiffPath, fileDiff);
1376
+ fs.writeFileSync(tempOneFileDiffPath, fileDiff);
781
1377
  trace7("file-temp-diff:write:done", tempOneFileDiffPath);
782
- let command = "";
783
1378
  trace7("file-command:create:start", file);
784
1379
  switch (service) {
785
1380
  case "gemini":
@@ -792,7 +1387,8 @@ async function main() {
792
1387
  command = createCodexCommand(tempOneFileDiffPath, reviewFormOneByOnePath);
793
1388
  break;
794
1389
  }
795
- trace7("file-command:create:done", file);
1390
+ trace7("file-command:create:done", `${file} | commandLength=${command.length}`);
1391
+ console.log(`\u23F3 [${index + 1}/${fileList.length}] \uB9AC\uBDF0 \uC9C4\uD589: ${file}`);
796
1392
  trace7("file-command:exec:start", file);
797
1393
  const { stdout } = await execAsync(command, { maxBuffer: 1024 * 1024 * 20 });
798
1394
  const result = stdout.toString();
@@ -804,13 +1400,12 @@ async function main() {
804
1400
  ${result}
805
1401
 
806
1402
  `;
807
- console.log(tempReport);
808
1403
  trace7("file-report:append:start", file);
809
- fs5.appendFileSync(savedReportPath, tempReport);
1404
+ fs.appendFileSync(savedReportPath, tempReport);
810
1405
  trace7("file-report:append:done", file);
811
1406
  if (isTest) {
812
1407
  trace7("file-test-command:append:start", file);
813
- fs5.appendFileSync(savedReportPath, `
1408
+ fs.appendFileSync(savedReportPath, `
814
1409
 
815
1410
  ## \uC0AC\uC6A9\uB41C \uBA85\uB839\uC5B4
816
1411
 
@@ -819,26 +1414,52 @@ ${command}`);
819
1414
  }
820
1415
  trace7("file-review:end", file);
821
1416
  } catch (err) {
822
- trace7("file-review:catch", file);
1417
+ trace7("file-review:catch", `${file} | ${getErrorSummary(err)}`);
1418
+ const errorLogPath = writeErrorReport(err, {
1419
+ scope: "review-one-by-one:file",
1420
+ args: args4,
1421
+ extraSections: [
1422
+ {
1423
+ heading: "Execution Context",
1424
+ markdown: `\`\`\`json
1425
+ ${JSON.stringify(
1426
+ {
1427
+ service,
1428
+ selectedCommitSummary: selectedCommitSummary || null,
1429
+ file,
1430
+ command: command || null,
1431
+ tempOneFileDiffPath,
1432
+ savedDiffPath: savedDiffPath || null,
1433
+ savedReportPath: savedReportPath || null
1434
+ },
1435
+ null,
1436
+ 2
1437
+ )}
1438
+ \`\`\``
1439
+ }
1440
+ ]
1441
+ });
823
1442
  console.error(`\u274C Error reviewing file ${file}:`, err);
1443
+ if (errorLogPath) {
1444
+ console.error(`\u{1F4C4} \uC5D0\uB7EC \uB85C\uADF8 \uC800\uC7A5 \uC704\uCE58: ${errorLogPath}`);
1445
+ }
824
1446
  const errorReport = `### File: ${file}
825
1447
  \u274C Review Failed
826
1448
  \`\`\`
827
- ${err}
1449
+ ${getErrorSummary(err)}
828
1450
  \`\`\`
829
1451
 
830
1452
  `;
831
- fs5.appendFileSync(savedReportPath, errorReport);
1453
+ fs.appendFileSync(savedReportPath, errorReport);
832
1454
  try {
833
- const tempOneFileDiffPath = `temp_diff_${file.replace(/\//g, "_")}.txt`;
834
- if (fs5.existsSync(tempOneFileDiffPath)) {
1455
+ if (fs.existsSync(tempOneFileDiffPath)) {
835
1456
  trace7("file-temp-diff:delete:start(catch)", tempOneFileDiffPath);
836
1457
  deleteFile(tempOneFileDiffPath);
837
1458
  trace7("file-temp-diff:delete:done(catch)", tempOneFileDiffPath);
838
1459
  }
839
- } catch (e) {
840
- trace7("file-temp-diff:delete:failed(catch)", file);
841
- console.error(`\u274C Error deleting temp file for ${file}:`, e);
1460
+ } catch (cleanupError) {
1461
+ trace7("file-temp-diff:delete:failed(catch)", `${file} | ${getErrorSummary(cleanupError)}`);
1462
+ console.error(`\u274C Error deleting temp file for ${file}:`, cleanupError);
842
1463
  }
843
1464
  }
844
1465
  });
@@ -857,12 +1478,45 @@ ${err}
857
1478
  trace7("cleanup-temp-diff:done");
858
1479
  trace7("review-flow:end");
859
1480
  } catch (error) {
860
- trace7("review-flow:catch");
1481
+ trace7("review-flow:catch", getErrorSummary(error));
1482
+ let errorReportPath = "";
1483
+ trace7("cleanup-temp-diff:start(catch)");
1484
+ try {
1485
+ deleteTempDiff();
1486
+ trace7("cleanup-temp-diff:done(catch)");
1487
+ } catch (cleanupError) {
1488
+ trace7("cleanup-temp-diff:failed(catch)", getErrorSummary(cleanupError));
1489
+ console.error("\u26A0\uFE0F \uC784\uC2DC diff \uC815\uB9AC \uC911 \uC624\uB958\uAC00 \uBC1C\uC0DD\uD588\uC2B5\uB2C8\uB2E4.");
1490
+ console.error(cleanupError);
1491
+ }
1492
+ trace7("error-report:prepare", `service=${service || "unknown"}`);
1493
+ errorReportPath = writeErrorReport(error, {
1494
+ scope: "review-one-by-one",
1495
+ args: args4,
1496
+ extraSections: [
1497
+ {
1498
+ heading: "Execution Context",
1499
+ markdown: `\`\`\`json
1500
+ ${JSON.stringify(
1501
+ {
1502
+ service: service || null,
1503
+ selectedCommitSummary: selectedCommitSummary || null,
1504
+ tempDiffPath,
1505
+ savedDiffPath: savedDiffPath || null,
1506
+ savedReportPath: savedReportPath || null
1507
+ },
1508
+ null,
1509
+ 2
1510
+ )}
1511
+ \`\`\``
1512
+ }
1513
+ ]
1514
+ });
861
1515
  console.error("\u274C \uB9AC\uBDF0 \uB3C4\uC911 \uC624\uB958\uAC00 \uBC1C\uC0DD\uD588\uC2B5\uB2C8\uB2E4.");
862
1516
  console.error(error);
863
- trace7("cleanup-temp-diff:start(catch)");
864
- deleteTempDiff();
865
- trace7("cleanup-temp-diff:done(catch)");
1517
+ if (errorReportPath) {
1518
+ console.error(`\u{1F4C4} \uC5D0\uB7EC \uB85C\uADF8 \uC800\uC7A5 \uC704\uCE58: ${errorReportPath}`);
1519
+ }
866
1520
  process.exit(1);
867
1521
  }
868
1522
  }