sales-frontend-gemini-cli 0.4.2 → 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 +464 -52
  2. package/dist/common/helper.cjs.map +1 -1
  3. package/dist/common/helper.d.cts +85 -3
  4. package/dist/common/helper.d.ts +85 -3
  5. package/dist/common/helper.js +455 -53
  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 +50 -9
  10. package/dist/pr-review/claude/claude-commander.cjs.map +1 -1
  11. package/dist/pr-review/claude/claude-commander.js +50 -9
  12. package/dist/pr-review/claude/claude-commander.js.map +1 -1
  13. package/dist/pr-review/claude/installation-claude.cjs +41 -5
  14. package/dist/pr-review/claude/installation-claude.cjs.map +1 -1
  15. package/dist/pr-review/claude/installation-claude.js +41 -5
  16. package/dist/pr-review/claude/installation-claude.js.map +1 -1
  17. package/dist/pr-review/codex/codex-commander.cjs +49 -8
  18. package/dist/pr-review/codex/codex-commander.cjs.map +1 -1
  19. package/dist/pr-review/codex/codex-commander.js +49 -8
  20. package/dist/pr-review/codex/codex-commander.js.map +1 -1
  21. package/dist/pr-review/codex/installation-codex.cjs +41 -5
  22. package/dist/pr-review/codex/installation-codex.cjs.map +1 -1
  23. package/dist/pr-review/codex/installation-codex.js +41 -5
  24. package/dist/pr-review/codex/installation-codex.js.map +1 -1
  25. package/dist/pr-review/gemini/gemini-commander.cjs +75 -15
  26. package/dist/pr-review/gemini/gemini-commander.cjs.map +1 -1
  27. package/dist/pr-review/gemini/gemini-commander.js +75 -15
  28. package/dist/pr-review/gemini/gemini-commander.js.map +1 -1
  29. package/dist/pr-review/gemini/installation-gemini.cjs +41 -5
  30. package/dist/pr-review/gemini/installation-gemini.cjs.map +1 -1
  31. package/dist/pr-review/gemini/installation-gemini.js +41 -5
  32. package/dist/pr-review/gemini/installation-gemini.js.map +1 -1
  33. package/dist/pr-review/review-one-by-one.cjs +489 -95
  34. package/dist/pr-review/review-one-by-one.cjs.map +1 -1
  35. package/dist/pr-review/review-one-by-one.js +490 -96
  36. package/dist/pr-review/review-one-by-one.js.map +1 -1
  37. package/dist/pr-review/review.cjs +517 -94
  38. package/dist/pr-review/review.cjs.map +1 -1
  39. package/dist/pr-review/review.js +517 -94
  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,6 +1,6 @@
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';
@@ -8,14 +8,52 @@ import { inspect } from 'util';
8
8
 
9
9
  var __dirname = path.dirname(fileURLToPath(import.meta.url));
10
10
  var traceMessages = [];
11
- var rulesPath = path.resolve(__dirname, "../../src/common/rules/review-rules.md");
12
- var namingRulesPath = path.resolve(__dirname, "../../src/common/rules/naming-rule.md");
13
- var codingConventionRulesPath = path.resolve(__dirname, "../../src/common/rules/coding-convention.md");
14
- var reviewFormPath = path.resolve(__dirname, "../../src/common/form/review-form.md");
15
- path.resolve(__dirname, "../../src/common/form/review-form-one-by-one.md");
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");
16
52
  var REPORT_DIR = ".review-report";
17
53
  var tempDiffPath = "temp_diff.txt";
18
54
  var AIServices = ["gemini", "claude", "codex"];
55
+ var COMMIT_FETCH_LIMIT = 20;
56
+ var COMMIT_SELECTION_WINDOW = 8;
19
57
  var ignoreList = [
20
58
  "package.json",
21
59
  "*.yml",
@@ -39,6 +77,185 @@ function clearTraceMessages() {
39
77
  function getTraceMessages() {
40
78
  return [...traceMessages];
41
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) {
154
+ return "";
155
+ }
156
+ const tokens = tokenizeVisibleText(value);
157
+ const totalWidth = tokens.reduce((sum, token) => sum + token.visibleWidth, 0);
158
+ if (totalWidth <= maxWidth) {
159
+ return value;
160
+ }
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}`;
178
+ }
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`;
258
+ }
42
259
  function createTraceLogger(scope, args4 = process.argv.slice(2)) {
43
260
  const enabled = isTestMode(args4);
44
261
  return (step, detail) => {
@@ -250,13 +467,67 @@ ${JSON.stringify(AIServices, null, 2)}
250
467
  }
251
468
  );
252
469
  }
253
- function getGitDiffFilter() {
254
- const includeExtensions = ["*.ts", "*.tsx", "*.js", "*.jsx"];
255
- const excludePatterns = ignoreList.map((item) => `:(exclude)${item}`);
256
- const quote = (pattern) => `"${pattern}"`;
257
- const includeParams = includeExtensions.map(quote).join(" ");
258
- const excludeParams = excludePatterns.map(quote).join(" ");
259
- return { includeParams, excludeParams };
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];
260
531
  }
261
532
  function openReport(reportPath) {
262
533
  const resolvedPath = path.resolve(reportPath);
@@ -307,59 +578,166 @@ function openReport(reportPath) {
307
578
  helperTrace("open-report:unsupported-platform", platform);
308
579
  console.error(`\u26A0\uFE0F \uC9C0\uC6D0\uD558\uC9C0 \uC54A\uB294 \uD50C\uB7AB\uD3FC\uC785\uB2C8\uB2E4: ${platform}`);
309
580
  }
310
- function getDiffArgs() {
311
- const args4 = process.argv.slice(2);
312
- const commitIndex = args4.indexOf("--commit");
313
- const { includeParams, excludeParams } = getGitDiffFilter();
314
- helperTrace("diff-args:resolve:start", `args=${JSON.stringify(args4)}`);
315
- let diffArgs = "";
316
- if (commitIndex !== -1) {
317
- const commitHash = args4[commitIndex + 1];
318
- if (!commitHash) {
319
- helperTrace("diff-args:commit-hash-missing");
320
- exitWithError("\u274C \uCEE4\uBC0B \uD574\uC2DC\uAC00 \uC81C\uACF5\uB418\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4.", {
321
- scope: "helper:getDiffArgs",
322
- args: args4
323
- });
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;
324
626
  }
325
- const nextArg = args4[commitIndex + 2];
326
- let n = 0;
327
- if (nextArg && !nextArg.startsWith("--")) {
328
- n = parseInt(nextArg, 10);
329
- if (isNaN(n)) {
330
- n = 0;
331
- }
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;
332
653
  }
333
- helperTrace("diff-args:commit-mode", `${commitHash}~${n + 1} ${commitHash}`);
334
- console.log(`\u2139\uFE0F \uCEE4\uBC0B '${commitHash}' ${n > 0 ? ` \uD3EC\uD568 \uCD1D ${n + 1}\uAC1C\uC758 \uCEE4\uBC0B` : ""}\uC744 \uB9AC\uBDF0\uD569\uB2C8\uB2E4...`);
335
- diffArgs = `${commitHash}~${n + 1} ${commitHash}`;
336
- } else {
337
- try {
338
- helperTrace("diff-args:unstaged-check:start");
339
- const check = execSync(`git diff --name-only -- ${includeParams} ${excludeParams}`).toString();
340
- if (!check.trim()) {
341
- helperTrace("diff-args:unstaged-check:empty", "use HEAD~1 HEAD");
342
- 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...");
343
- diffArgs = "HEAD~1 HEAD";
344
- } else {
345
- helperTrace("diff-args:unstaged-check:has-changes", `length=${check.length}`);
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);
346
682
  }
347
- } catch (error) {
348
- helperTrace("diff-args:unstaged-check:failed", getErrorSummary(error));
349
- }
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 [];
350
720
  }
351
- helperTrace("diff-args:resolve:done", diffArgs || "(default)");
352
- 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
+ );
353
730
  }
354
731
  async function showSelectionAIService() {
355
732
  const selectedServiceFromArgs = parseServiceFromArgs();
356
733
  if (selectedServiceFromArgs) {
357
734
  helperTrace("show-selection:from-args", selectedServiceFromArgs);
358
735
  console.log(`
359
- \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)
360
737
  `);
361
738
  return selectedServiceFromArgs;
362
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.");
363
741
  helperTrace("show-selection:interactive:start");
364
742
  let selectedIndex = 0;
365
743
  const rl = readline.createInterface({
@@ -377,11 +755,12 @@ async function showSelectionAIService() {
377
755
  helperTrace("show-selection:interactive:render", AIServices[selectedIndex] || "unknown");
378
756
  readline.clearScreenDown(process.stdout);
379
757
  process.stdout.write(
380
- "\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
+ `
381
760
  );
382
761
  AIServices.forEach((service, index) => {
383
762
  if (index === selectedIndex) {
384
- 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}
385
764
  `);
386
765
  } else {
387
766
  process.stdout.write(` \u25EF ${service}
@@ -411,7 +790,7 @@ async function showSelectionAIService() {
411
790
  rl.close();
412
791
  process.stdout.write("\x1B[?25h");
413
792
  console.log(`
414
- \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.
415
794
  `);
416
795
  const result = AIServices[selectedIndex];
417
796
  if (result) {
@@ -438,6 +817,11 @@ function getArgValue(flag) {
438
817
  }
439
818
  return args[index + 1];
440
819
  }
820
+ function printNotice(message) {
821
+ if (args.includes("--test")) {
822
+ console.warn(message);
823
+ }
824
+ }
441
825
  function toUnique(values) {
442
826
  const seen = /* @__PURE__ */ new Set();
443
827
  return values.filter((value) => {
@@ -461,11 +845,11 @@ function resolveReasoningEffort() {
461
845
  const normalized = normalizeEffort(customReasoningEffort);
462
846
  trace("reasoning:custom", `${customReasoningEffort} -> ${normalized}`);
463
847
  if (customReasoningEffort === "minimal") {
464
- 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.");
465
849
  }
466
850
  return normalized;
467
851
  }
468
- console.warn(
852
+ printNotice(
469
853
  `\u26A0\uFE0F \uC9C0\uC6D0\uB418\uC9C0 \uC54A\uB294 reasoning effort(${customReasoningEffort})\uC785\uB2C8\uB2E4. allowed: ${ALLOWED_REASONING_EFFORTS.join(
470
854
  ", "
471
855
  )}`
@@ -508,7 +892,7 @@ function buildClaudeExecCommand(options) {
508
892
  const modelOption = model ? `--model ${shellQuote(model)}` : "";
509
893
  const fallbackOption = model && fallbackModel ? `--fallback-model ${shellQuote(fallbackModel)}` : "";
510
894
  const effortOption = `--effort ${shellQuote(effort)}`;
511
- 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(" ");
512
896
  return `cat ${shellQuote(tempDiffPath2)} | claude ${[
513
897
  modelOption,
514
898
  fallbackOption,
@@ -541,13 +925,13 @@ var createClaudeCommand = (tempDiffPath2, reviewFormPath2) => {
541
925
  trace("model:candidates", modelCandidates.join(", "));
542
926
  trace("command:candidates:count", String(modelCandidates.length + 1));
543
927
  if (customModel) {
544
- console.warn(
928
+ printNotice(
545
929
  `\u26A0\uFE0F \uCEE4\uC2A4\uD140 \uBAA8\uB378(${customModel})\uC744 \uC6B0\uC120 \uC2DC\uB3C4\uD558\uACE0 \uC2E4\uD328\uD558\uBA74 alias(${aliasFallbacks.join(
546
930
  " -> "
547
931
  )}) \uBC0F \uAE30\uBCF8 \uBAA8\uB378 \uC21C\uC73C\uB85C \uD3F4\uBC31\uD569\uB2C8\uB2E4.`
548
932
  );
549
933
  } else {
550
- console.warn(
934
+ printNotice(
551
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.`
552
936
  );
553
937
  }
@@ -627,6 +1011,11 @@ function getArgValue2(flag) {
627
1011
  }
628
1012
  return args2[index + 1];
629
1013
  }
1014
+ function printNotice2(message) {
1015
+ if (args2.includes("--test")) {
1016
+ console.warn(message);
1017
+ }
1018
+ }
630
1019
  function resolveReasoningEffort2() {
631
1020
  const customReasoningEffort = getArgValue2("--reasoning-effort");
632
1021
  if (customReasoningEffort) {
@@ -634,7 +1023,7 @@ function resolveReasoningEffort2() {
634
1023
  trace3("reasoning:custom", customReasoningEffort);
635
1024
  return customReasoningEffort;
636
1025
  }
637
- console.warn(
1026
+ printNotice2(
638
1027
  `\u26A0\uFE0F \uC9C0\uC6D0\uB418\uC9C0 \uC54A\uB294 reasoning effort(${customReasoningEffort})\uC785\uB2C8\uB2E4. allowed: ${ALLOWED_REASONING_EFFORTS2.join(
639
1028
  ", "
640
1029
  )}`
@@ -678,14 +1067,14 @@ ${reviewFormLine || "- (\uC5C6\uC74C)"}
678
1067
  trace3("prompt:prepared", `length=${prompt.length}`);
679
1068
  let command = "";
680
1069
  if (customModel) {
681
- 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.");
682
1071
  trace3("model:custom", customModel);
683
1072
  command = buildCodexExecCommand(prompt, reasoningEffort, customModel);
684
1073
  } else {
685
1074
  const preferredModelAlias = "gpt-5";
686
1075
  const aliasCommand = buildCodexExecCommand(prompt, reasoningEffort, preferredModelAlias);
687
1076
  const fallbackCommand = buildCodexExecCommand(prompt, reasoningEffort);
688
- console.warn(
1077
+ printNotice2(
689
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.`
690
1079
  );
691
1080
  trace3("model:alias-first", preferredModelAlias);
@@ -746,6 +1135,11 @@ function getArgValue3(flag) {
746
1135
  }
747
1136
  return args3[index + 1];
748
1137
  }
1138
+ function printNotice3(message) {
1139
+ if (args3.includes("--test")) {
1140
+ console.warn(message);
1141
+ }
1142
+ }
749
1143
  function toUnique2(values) {
750
1144
  const seen = /* @__PURE__ */ new Set();
751
1145
  return values.filter((value) => {
@@ -763,7 +1157,7 @@ function resolveReasoningEffort3() {
763
1157
  trace5("reasoning:custom", customReasoningEffort);
764
1158
  return customReasoningEffort;
765
1159
  }
766
- console.warn(
1160
+ printNotice3(
767
1161
  `\u26A0\uFE0F \uC9C0\uC6D0\uB418\uC9C0 \uC54A\uB294 reasoning effort(${customReasoningEffort})\uC785\uB2C8\uB2E4. allowed: ${ALLOWED_REASONING_EFFORTS3.join(
768
1162
  ", "
769
1163
  )}`
@@ -825,38 +1219,57 @@ function buildGeminiExecCommand(prompt, model) {
825
1219
  const modelOption = model ? `--model ${shellQuote3(model)}` : "";
826
1220
  return `gemini ${[modelOption, "-p", shellQuote3(prompt)].filter(Boolean).join(" ")}`;
827
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
+ }
828
1232
  var createGeminiCommand = (tempDiffPath2, reviewFormPath2) => {
829
1233
  trace5("createGeminiCommand:start", `tempDiffPath=${tempDiffPath2}, reviewFormPath=${reviewFormPath2}`);
830
1234
  const customModel = getArgValue3("--model");
831
1235
  const reasoningEffort = resolveReasoningEffort3();
832
1236
  const primaryAlias = resolvePrimaryAlias2(reasoningEffort);
833
1237
  const aliasFallbacks = toUnique2(getAliasFallbacks2(primaryAlias));
1238
+ const resolvedTempDiffPath = path.resolve(tempDiffPath2);
1239
+ const resolvedReviewFormPath = path.resolve(reviewFormPath2);
834
1240
  const rules = [
835
1241
  { path: rulesPath, display: "\uB8F0\uC14B" },
836
1242
  { path: namingRulesPath, display: "\uB124\uC774\uBC0D \uADDC\uCE59" },
837
1243
  { path: codingConventionRulesPath, display: "\uCF54\uB529 \uCEE8\uBCA4\uC158" }
838
1244
  ];
839
- const validRules = rules.filter((rule) => fs.existsSync(rule.path)).map((rule) => `@${rule.path}`).join(", ");
840
- const rulesCount = validRules ? validRules.split(",").length : 0;
841
- trace5("rules:loaded", `count=${rulesCount}`);
842
- const reviewFormRef = fs.existsSync(reviewFormPath2) ? `@${reviewFormPath2}` : "(\uC5C6\uC74C)";
843
- 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");
844
1250
  const reasoningInstruction = getReasoningInstruction(reasoningEffort);
845
- const prompt = `\uB2E4\uC74C \uADDC\uCE59\uB4E4\uC744 \uCC38\uACE0\uD574\uC11C(${validRules || "(\uC5C6\uC74C)"}) \uC774 diff(@${tempDiffPath2})\uB97C \uB9AC\uBDF0\uD574\uC918.
846
- \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.
847
1260
  \uCD94\uB860 \uAC15\uB3C4 \uC9C0\uCE68: ${reasoningInstruction}`;
848
1261
  trace5("prompt:prepared", `length=${prompt.length}`);
849
1262
  const modelCandidates = toUnique2(customModel ? [customModel, ...aliasFallbacks] : aliasFallbacks);
850
1263
  trace5("model:candidates", modelCandidates.join(", "));
851
1264
  trace5("command:candidates:count", String(modelCandidates.length + 1));
852
1265
  if (customModel) {
853
- console.warn(
1266
+ printNotice3(
854
1267
  `\u26A0\uFE0F \uCEE4\uC2A4\uD140 \uBAA8\uB378(${customModel})\uC744 \uC6B0\uC120 \uC2DC\uB3C4\uD558\uACE0 \uC2E4\uD328\uD558\uBA74 alias(${aliasFallbacks.join(
855
1268
  " -> "
856
1269
  )}) \uBC0F \uAE30\uBCF8 \uBAA8\uB378 \uC21C\uC73C\uB85C \uD3F4\uBC31\uD569\uB2C8\uB2E4.`
857
1270
  );
858
1271
  } else {
859
- console.warn(
1272
+ printNotice3(
860
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.`
861
1274
  );
862
1275
  }
@@ -915,6 +1328,8 @@ async function main() {
915
1328
  let savedDiffPath = "";
916
1329
  let savedReportPath = "";
917
1330
  let service = "";
1331
+ let selectedCommitSummary = "";
1332
+ let reviewTargetFiles = [];
918
1333
  try {
919
1334
  trace7("service-selection:start");
920
1335
  service = await showSelectionAIService();
@@ -938,30 +1353,34 @@ async function main() {
938
1353
  }
939
1354
  trace7("review-flow:start");
940
1355
  console.log("\u{1F680} AI Code Review\uB97C \uC2DC\uC791\uD569\uB2C8\uB2E4...");
941
- const nowStr = getNowString();
942
- trace7("timestamp:created", nowStr);
943
- trace7("report-dir:create:start");
944
- createReportDirectory();
945
- trace7("report-dir:create:done");
946
- trace7("diff-args:build:start");
947
- const diffArgs = getDiffArgs();
948
- trace7("diff-args:build:done", `diffArgs=${diffArgs || "(default)"}`);
949
- let diff = "";
950
- const { includeParams, excludeParams } = getGitDiffFilter();
951
- trace7("diff-filter:loaded", `include=${includeParams} | exclude=${excludeParams}`);
952
- try {
953
- trace7("git-diff:run");
954
- diff = execSync(`git diff ${diffArgs} -- ${includeParams} ${excludeParams}`).toString();
955
- trace7("git-diff:done", `length=${diff.length}`);
956
- } catch (error) {
957
- trace7("git-diff:error", getErrorSummary(error));
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);
958
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}`);
959
1373
  if (!diff.trim() && !isTest) {
960
1374
  trace7("empty-diff:exit");
961
- 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.");
962
1376
  deleteTempDiff();
963
1377
  process.exit(0);
964
1378
  }
1379
+ const nowStr = getNowString();
1380
+ trace7("timestamp:created", nowStr);
1381
+ trace7("report-dir:create:start");
1382
+ createReportDirectory();
1383
+ trace7("report-dir:create:done");
965
1384
  trace7("temp-diff:write:start", tempDiffPath);
966
1385
  fs.writeFileSync(tempDiffPath, diff);
967
1386
  trace7("temp-diff:write:done");
@@ -983,9 +1402,11 @@ async function main() {
983
1402
  }
984
1403
  trace7("command:create:done");
985
1404
  trace7("command:exec:start");
986
- 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;
987
1409
  trace7("command:exec:done", `resultLength=${result.length}`);
988
- console.log(result);
989
1410
  trace7("report:write:start");
990
1411
  savedReportPath = getNextFilePath(REPORT_DIR, nowStr, ".md");
991
1412
  fs.writeFileSync(savedReportPath, result);
@@ -1033,6 +1454,8 @@ ${command}`);
1033
1454
  ${JSON.stringify(
1034
1455
  {
1035
1456
  service,
1457
+ selectedCommitSummary: selectedCommitSummary || null,
1458
+ reviewTargetFiles,
1036
1459
  command: command || null,
1037
1460
  tempDiffPath,
1038
1461
  savedDiffPath: savedDiffPath || null,