@kyubiware/commit-mint 0.5.0 → 0.5.1

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.
package/README.md CHANGED
@@ -68,7 +68,6 @@ On first run, you'll be prompted for a `GROQ_API_KEY` if it's not set in `~/.com
68
68
  - **Hook failures handled in-flow.** When pre-commit hooks fail, you get a parsed error summary and a menu to copy, skip, retry, or edit. No raw stderr dumps.
69
69
  - **Built-in pre-commit checks.** Define checks in a cmint config file and run them before AI generation. A failing check never wastes an API call.
70
70
  - **Message caching on failure.** A failed commit caches its message. Fix the error, run `cmint --retry`, and pick up exactly where you left off.
71
- - **Review before you commit.** Every message can be accepted, edited, or reviewed with OpenCode before it hits the repo.
72
71
 
73
72
  ## Auto mode (`-a`)
74
73
 
@@ -111,9 +110,6 @@ cmint -H "refactoring auth module"
111
110
  # Retry last failed commit (uses cached message)
112
111
  cmint -r
113
112
 
114
- # Review staged changes with AI
115
- cmint -R
116
-
117
113
  # Skip pre-commit checks
118
114
  cmint -n
119
115
 
@@ -132,7 +128,6 @@ cmint config set model openai/gpt-oss-20b
132
128
  | `-m, --message` | Provide a commit message directly (skip AI) |
133
129
  | `-H, --hint` | Add context hint for AI message generation |
134
130
  | `-r, --retry` | Retry the last failed commit |
135
- | `-R, --review` | Review staged changes with a coding model |
136
131
  | `-n, --noCheck` | Skip pre-commit checks |
137
132
  | `-d, --debug` | Enable debug output |
138
133
  | `-h, --help` | Show help |
@@ -144,7 +139,6 @@ Before every commit, choose what to do with the generated message:
144
139
 
145
140
  - **Use as-is** — accept the AI-generated message
146
141
  - **Edit** — modify the message in a prompt
147
- - **Review with OpenCode** — run a code review on staged changes before committing
148
142
  - **Cancel** — exit (message is cached for `--retry`)
149
143
 
150
144
  ## Recovery menu
@@ -236,10 +230,6 @@ When a check fails, you get a menu with three options:
236
230
 
237
231
  Pass `--noCheck` or `-n` to skip checks entirely.
238
232
 
239
- ## Code review
240
-
241
- Run `cmint --review` or `cmint -R` to review staged changes without committing. commit-mint checks for [OpenCode](https://github.com/opencode-ai/opencode) first, then falls back to Groq API. The review covers bugs, security issues, performance problems, and edge cases.
242
-
243
233
  ## Configuration
244
234
 
245
235
  Stored in `~/.commit-mint` (INI format):
@@ -278,7 +268,6 @@ cmint --help
278
268
  --auto, -a Auto-group files into commits, accept all messages (default: false)
279
269
  --message, -m Provide a commit message directly (skip AI generation)
280
270
  --hint, -H Add context hint for AI commit message generation
281
- --review, -R Review staged changes with a coding model (default: false)
282
271
  --noCheck, -n Skip pre-commit checks (default: false)
283
272
  --debug, -d Enable debug output (default: false)
284
273
  --help, -h Show help
@@ -301,13 +290,11 @@ commit-mint/
301
290
  │ ├── commands/
302
291
  │ │ ├── commit.ts # Main commit flow orchestrator
303
292
  │ │ ├── auto-group.ts # Auto-group multi-commit flow
304
- │ │ ├── review.ts # Code review command
305
293
  │ │ └── config.ts # Config get/set subcommand
306
294
  │ ├── services/
307
295
  │ │ ├── git.ts # Git operations (stage, commit, diff, HEAD)
308
296
  │ │ ├── ai.ts # Groq AI commit message generation (3-tier diff compression)
309
297
  │ │ ├── grouping.ts # AI-powered file grouping into logical commits
310
- │ │ ├── review-ai.ts # AI code review via Groq
311
298
  │ │ ├── hooks.ts # Hook error parser (lint-staged, biome, tsc, etc.)
312
299
  │ │ ├── checks.ts # Pre-commit checks via cmint config files (glob matching, command execution)
313
300
  │ │ ├── config.ts # INI config read/write at ~/.commit-mint
@@ -315,7 +302,7 @@ commit-mint/
315
302
  │ ├── ui/
316
303
  │ │ ├── menu.ts # Interactive recovery TUI + staging menu
317
304
  │ │ ├── grouping.ts # Grouping confirmation UI
318
- │ │ └── review-message.ts # Message review step (use/edit/review/cancel)
305
+ │ │ └── review-message.ts # Message review step (use/edit/cancel)
319
306
  │ └── utils/
320
307
  │ ├── cache.ts # Commit message persistence at ~/.cache/commit-mint/
321
308
  │ └── debug.ts # Timestamped debug logging to stderr
package/dist/cli.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { cli, command } from "cleye";
3
3
  import * as p from "@clack/prompts";
4
- import { intro, isCancel, log, note, outro, select, spinner } from "@clack/prompts";
4
+ import { intro, isCancel, log, outro, spinner } from "@clack/prompts";
5
5
  import { bold, cyan, dim, green, red, yellow } from "kolorist";
6
6
  import { access, constants, mkdir, readFile, writeFile } from "node:fs/promises";
7
7
  import os from "node:os";
@@ -9,10 +9,10 @@ import { extname, join } from "node:path";
9
9
  import ini from "ini";
10
10
  import Groq from "groq-sdk";
11
11
  import { execa } from "execa";
12
- import { spawn } from "node:child_process";
13
12
  import { createHash } from "node:crypto";
14
13
  import { readFileSync } from "node:fs";
15
14
  import picomatch from "picomatch";
15
+ import { spawn } from "node:child_process";
16
16
  //#region \0rolldown/runtime.js
17
17
  var __defProp = Object.defineProperty;
18
18
  var __exportAll = (all, no_symbols) => {
@@ -28,7 +28,7 @@ var __exportAll = (all, no_symbols) => {
28
28
  //#region package.json
29
29
  var package_default = {
30
30
  name: "@kyubiware/commit-mint",
31
- version: "0.5.0",
31
+ version: "0.5.1",
32
32
  description: "🌿 A commit tool that actually handles hook failures",
33
33
  type: "module",
34
34
  bin: { "cmint": "./dist/cli.mjs" },
@@ -567,43 +567,80 @@ async function attemptCommitNoVerify(message, onProgress) {
567
567
  return attemptCommit(message, ["--no-verify"], onProgress);
568
568
  }
569
569
  //#endregion
570
- //#region src/services/clipboard.ts
571
- async function copyToClipboard(content) {
572
- for (const [cmd, args] of [
573
- ["wl-copy", []],
574
- ["xclip", ["-selection", "clipboard"]],
575
- ["xsel", ["--clipboard", "--input"]],
576
- ["pbcopy", []]
577
- ]) try {
578
- if (await new Promise((resolve) => {
579
- const child = spawn(cmd, args, { stdio: [
580
- "pipe",
581
- "ignore",
582
- "ignore"
583
- ] });
584
- let settled = false;
585
- const done = (result) => {
586
- if (settled) return;
587
- settled = true;
588
- resolve(result);
589
- };
590
- child.on("error", () => done(false));
591
- child.on("exit", (code) => {
592
- if (code !== 0) done(false);
593
- });
594
- child.stdin.write(content, (err) => {
595
- if (err) {
596
- done(false);
597
- return;
570
+ //#region src/ui/review-message.ts
571
+ async function reviewCommitMessage(message) {
572
+ const { select, text } = await import("@clack/prompts");
573
+ while (true) {
574
+ const review = await select({
575
+ message: `Review commit message:\n\n ${bold(message)}\n`,
576
+ options: [
577
+ {
578
+ label: "Use as-is",
579
+ value: "use"
580
+ },
581
+ {
582
+ label: "Edit",
583
+ value: "edit"
584
+ },
585
+ {
586
+ label: "Cancel",
587
+ value: "cancel"
598
588
  }
599
- child.stdin.end(() => {
600
- child.unref();
601
- done(true);
602
- });
589
+ ]
590
+ });
591
+ if (isCancel(review) || review === "cancel") {
592
+ debug("User cancelled at review step");
593
+ return null;
594
+ }
595
+ if (review === "use") {
596
+ debug("User accepted message");
597
+ return message;
598
+ }
599
+ if (review === "edit") {
600
+ debug("User chose to edit message");
601
+ const edited = await text({
602
+ message: "Edit commit message:",
603
+ initialValue: message,
604
+ validate: (v) => v?.trim() ? void 0 : "Message cannot be empty"
603
605
  });
604
- })) return true;
605
- } catch {}
606
- return false;
606
+ if (isCancel(edited)) continue;
607
+ message = String(edited).trim();
608
+ debug("Edited message:", message);
609
+ }
610
+ }
611
+ }
612
+ //#endregion
613
+ //#region src/utils/cache.ts
614
+ const CACHE_DIR = join(os.homedir(), ".cache", "commit-mint");
615
+ function repoHash(repoPath) {
616
+ return createHash("sha256").update(repoPath).digest("hex").slice(0, 12);
617
+ }
618
+ function cachePath(repoPath) {
619
+ return join(CACHE_DIR, `${repoHash(repoPath)}.json`);
620
+ }
621
+ async function saveCachedCommit(repoPath, message) {
622
+ await mkdir(CACHE_DIR, { recursive: true });
623
+ const data = {
624
+ message,
625
+ timestamp: Date.now(),
626
+ repoPath
627
+ };
628
+ const path = cachePath(repoPath);
629
+ debug("saveCachedCommit: saving to %s", path);
630
+ await writeFile(path, JSON.stringify(data, null, 2), "utf8");
631
+ }
632
+ async function loadCachedCommit(repoPath) {
633
+ const path = cachePath(repoPath);
634
+ debug("loadCachedCommit: loading from %s", path);
635
+ try {
636
+ const raw = await readFile(path, "utf8");
637
+ const data = JSON.parse(raw);
638
+ debug("loadCachedCommit: found message from %s", new Date(data.timestamp).toISOString());
639
+ return data;
640
+ } catch {
641
+ debug("loadCachedCommit: no cached commit found");
642
+ return null;
643
+ }
607
644
  }
608
645
  //#endregion
609
646
  //#region src/services/ai.ts
@@ -778,330 +815,6 @@ async function generateCommitMessage(diff, options) {
778
815
  }
779
816
  }
780
817
  //#endregion
781
- //#region src/services/review-ai.ts
782
- function buildReviewSystemPrompt() {
783
- return [
784
- "You are an expert code reviewer. Review the following staged git diff.",
785
- "",
786
- "Focus on finding:",
787
- "1. **Bugs** — logic errors, off-by-one, race conditions, null pointer risks",
788
- "2. **Security issues** — injection, exposure of secrets, missing validation, CSRF, XSS",
789
- "3. **Performance problems** — unnecessary work, large allocations in hot paths",
790
- "4. **Code quality** — readability, maintainability, error handling gaps",
791
- "5. **Edge cases** — missing boundary checks, empty states, error states",
792
- "",
793
- "For each issue found, use this format:",
794
- "- SEVERITY: [critical|major|minor|suggestion]",
795
- "- LOCATION: <file-path>:<line-number>",
796
- "- ISSUE: <description>",
797
- "- FIX: <suggested resolution>",
798
- "",
799
- "Separate issues with a blank line.",
800
- "",
801
- "If you find NO issues at all, respond with exactly: NO_ISSUES_FOUND",
802
- "",
803
- "Be thorough but practical. Only flag real problems — not style preferences or nitpicks."
804
- ].join("\n");
805
- }
806
- function buildReviewPrompt(diff, files, statSummary) {
807
- const parts = [];
808
- parts.push(`Review the following staged changes (${files.length} files):`);
809
- parts.push("");
810
- parts.push(statSummary);
811
- parts.push("");
812
- parts.push("```diff");
813
- parts.push(diff);
814
- parts.push("```");
815
- return parts.join("\n");
816
- }
817
- async function generateCodeReview(diff, files, options) {
818
- debug("generateCodeReview: model=%s, files=%d", options.model ?? "default", files.length);
819
- const timeoutMs = options.timeout ?? 6e4;
820
- const { client, model } = createProvider({
821
- provider: options.provider ?? "groq",
822
- apiKey: options.apiKey,
823
- modelOverride: options.model,
824
- timeout: timeoutMs,
825
- baseURLOverride: options.proxy
826
- });
827
- const compressedDiff = compressDiff(diff);
828
- const statSummary = buildStatSummary(diff);
829
- const systemPrompt = buildReviewSystemPrompt();
830
- const userPrompt = buildReviewPrompt(compressedDiff, files, statSummary);
831
- debug("Code review: %d chars → %d chars, system=%d chars, user=%d chars", diff.length, compressedDiff.length, systemPrompt.length, userPrompt.length);
832
- try {
833
- const completion = await client.chat.completions.create({
834
- messages: [{
835
- role: "system",
836
- content: systemPrompt
837
- }, {
838
- role: "user",
839
- content: userPrompt
840
- }],
841
- model,
842
- temperature: .3,
843
- max_tokens: 4096
844
- });
845
- const rawContent = completion.choices[0]?.message?.content;
846
- const content = extractContentText(rawContent);
847
- debug("generateCodeReview response: choices=%d, finishReason=%s, contentLen=%d", completion.choices.length, completion.choices[0]?.finish_reason ?? "(none)", content.length);
848
- if (!content) {
849
- const reasoning = completion.choices[0]?.message?.reasoning;
850
- if (reasoning) {
851
- const derived = deriveMessageFromReasoning(reasoning);
852
- if (derived) {
853
- debug("generateCodeReview: derived from reasoning");
854
- return derived;
855
- }
856
- }
857
- return "NO_ISSUES_FOUND";
858
- }
859
- return content;
860
- } catch (error) {
861
- debug("generateCodeReview error: %s", error instanceof Error ? error.message : String(error));
862
- throw mapGroqError(error, options.provider ? formatProviderName(options.provider) : void 0);
863
- }
864
- }
865
- //#endregion
866
- //#region src/commands/review.ts
867
- async function reviewCommand() {
868
- debug("reviewCommand called");
869
- await assertGitRepo();
870
- const s = spinner();
871
- s.start("Staging all changes...");
872
- await stageAll();
873
- s.stop("Changes staged");
874
- const diffResult = await getStagedDiff();
875
- if (!diffResult) {
876
- outro(dim("No changes to review."));
877
- return;
878
- }
879
- if ("excludedFiles" in diffResult) {
880
- outro(dim("Staged files are all excluded from review."));
881
- return;
882
- }
883
- intro("🌿 commit-mint — code review");
884
- log.info(diffResult.files.map((f) => ` ${f}`).join("\n"));
885
- const report = await isOpenCodeAvailable() ? await reviewWithOpenCode(diffResult.diff, diffResult.files) : await reviewWithGroq(diffResult.diff, diffResult.files);
886
- if (report !== "NO_ISSUES_FOUND" && report.trim().length > 0) {
887
- note(report, red(bold("Review findings")));
888
- await offerClipboardCopy(report);
889
- } else outro(green("No issues found. Looks good!"));
890
- }
891
- async function offerClipboardCopy(report) {
892
- const shouldCopy = await select({
893
- message: "Copy review report to clipboard?",
894
- options: [{
895
- label: "Yes, copy to clipboard",
896
- value: "yes"
897
- }, {
898
- label: "No",
899
- value: "no"
900
- }]
901
- });
902
- if (isCancel(shouldCopy) || shouldCopy === "no") {
903
- outro(dim("Done."));
904
- return;
905
- }
906
- if (await copyToClipboard(report)) outro(green("Report copied to clipboard. You can paste it anywhere for fixes."));
907
- else outro(red("Failed to copy to clipboard. Install xclip, wl-copy, or xsel."));
908
- }
909
- async function isOpenCodeAvailable() {
910
- try {
911
- const { exitCode } = await import("execa").then((m) => m.execa("which", ["opencode"], { reject: false }));
912
- return exitCode === 0;
913
- } catch {
914
- return false;
915
- }
916
- }
917
- async function reviewWithGroq(diff, files) {
918
- const config = await readConfig();
919
- const provider = isValidProvider(config.provider ?? "groq") ? config.provider : "groq";
920
- const apiKey = await getProviderApiKey(provider);
921
- const model = getModelForProvider(config, provider, PROVIDER_CONFIGS[provider].defaultModel);
922
- const s = spinner();
923
- s.start(`Reviewing with ${formatProviderName(provider)}...`);
924
- try {
925
- const report = await generateCodeReview(diff, files, {
926
- apiKey,
927
- model,
928
- timeout: config.timeout ? Number.parseInt(config.timeout, 10) : void 0,
929
- provider,
930
- proxy: config.proxy
931
- });
932
- s.stop("Review complete");
933
- return report;
934
- } catch (err) {
935
- s.stop(red("Review failed."));
936
- debug("reviewWithGroq error:", err instanceof Error ? err.message : String(err));
937
- throw err;
938
- }
939
- }
940
- async function reviewWithOpenCode(diff, files) {
941
- const s = spinner();
942
- s.start("Running OpenCode review...");
943
- try {
944
- const repoRoot = await getRepoRoot();
945
- const prompt = [
946
- "Review the staged changes in this git repository.",
947
- "Analyze the code diff for bugs, security issues, performance problems,",
948
- "code quality issues, and missing edge cases.",
949
- "",
950
- `Files changed (${files.length}):`,
951
- ...files.map((f) => ` - ${f}`),
952
- "",
953
- "```diff",
954
- diff.slice(0, 15e3),
955
- "```",
956
- "",
957
- "Provide a structured report with severity, location, issue, and fix suggestion for each finding.",
958
- "If no issues found, respond with: NO_ISSUES_FOUND"
959
- ].join("\n");
960
- const { stdout } = await import("execa").then((m) => m.execa("opencode", [
961
- "run",
962
- prompt,
963
- "--dir",
964
- repoRoot
965
- ], {
966
- timeout: 12e4,
967
- reject: false
968
- }));
969
- s.stop("Review complete");
970
- return stdout || "OpenCode review completed but no output captured.";
971
- } catch (err) {
972
- s.stop(red("OpenCode review failed."));
973
- debug("reviewWithOpenCode error:", err instanceof Error ? err.message : String(err));
974
- throw new Error(`OpenCode review failed: ${err instanceof Error ? err.message : String(err)}`);
975
- }
976
- }
977
- //#endregion
978
- //#region src/ui/review-message.ts
979
- async function runCodeReview() {
980
- const diffResult = await getStagedDiff();
981
- if (!diffResult || "excludedFiles" in diffResult) {
982
- outro(dim("No staged changes to review."));
983
- return;
984
- }
985
- const opencodeAvailable = await isOpenCodeAvailable();
986
- const config = await readConfig();
987
- const provider = isValidProvider(config.provider ?? "groq") ? config.provider : "groq";
988
- const s = spinner();
989
- s.start(opencodeAvailable ? "Running OpenCode review..." : `Running ${formatProviderName(provider)} review...`);
990
- try {
991
- const report = opencodeAvailable ? await reviewWithOpenCode(diffResult.diff, diffResult.files) : await reviewWithGroq(diffResult.diff, diffResult.files);
992
- s.stop("Review complete");
993
- await showReviewResults(report);
994
- } catch (err) {
995
- s.stop(red("Review failed."));
996
- debug("Code review error:", err instanceof Error ? err.message : String(err));
997
- outro(red(err instanceof Error ? err.message : String(err)));
998
- }
999
- }
1000
- async function showReviewResults(report) {
1001
- const { note: clackNote, select: clackSelect } = await import("@clack/prompts");
1002
- if (!(report !== "NO_ISSUES_FOUND" && report.trim().length > 0)) {
1003
- log.info(green("No issues found."));
1004
- return;
1005
- }
1006
- clackNote(report, red(bold("Review findings")));
1007
- const shouldCopy = await clackSelect({
1008
- message: "Copy review report to clipboard?",
1009
- options: [{
1010
- label: "Yes, copy to clipboard",
1011
- value: "yes"
1012
- }, {
1013
- label: "No",
1014
- value: "no"
1015
- }]
1016
- });
1017
- if (isCancel(shouldCopy) || shouldCopy !== "yes") return;
1018
- if (await copyToClipboard(report)) log.info(green("Report copied to clipboard."));
1019
- else log.warn(red("Failed to copy to clipboard."));
1020
- }
1021
- async function reviewCommitMessage(message) {
1022
- const { select, text } = await import("@clack/prompts");
1023
- while (true) {
1024
- const review = await select({
1025
- message: `Review commit message:\n\n ${bold(message)}\n`,
1026
- options: [
1027
- {
1028
- label: "Use as-is",
1029
- value: "use"
1030
- },
1031
- {
1032
- label: "Edit",
1033
- value: "edit"
1034
- },
1035
- {
1036
- label: "Review with OpenCode",
1037
- value: "review"
1038
- },
1039
- {
1040
- label: "Cancel",
1041
- value: "cancel"
1042
- }
1043
- ]
1044
- });
1045
- if (isCancel(review) || review === "cancel") {
1046
- debug("User cancelled at review step");
1047
- return null;
1048
- }
1049
- if (review === "use") {
1050
- debug("User accepted message");
1051
- return message;
1052
- }
1053
- if (review === "edit") {
1054
- debug("User chose to edit message");
1055
- const edited = await text({
1056
- message: "Edit commit message:",
1057
- initialValue: message,
1058
- validate: (v) => v?.trim() ? void 0 : "Message cannot be empty"
1059
- });
1060
- if (isCancel(edited)) continue;
1061
- message = String(edited).trim();
1062
- debug("Edited message:", message);
1063
- continue;
1064
- }
1065
- if (review === "review") {
1066
- debug("User chose to review code");
1067
- await runCodeReview();
1068
- }
1069
- }
1070
- }
1071
- //#endregion
1072
- //#region src/utils/cache.ts
1073
- const CACHE_DIR = join(os.homedir(), ".cache", "commit-mint");
1074
- function repoHash(repoPath) {
1075
- return createHash("sha256").update(repoPath).digest("hex").slice(0, 12);
1076
- }
1077
- function cachePath(repoPath) {
1078
- return join(CACHE_DIR, `${repoHash(repoPath)}.json`);
1079
- }
1080
- async function saveCachedCommit(repoPath, message) {
1081
- await mkdir(CACHE_DIR, { recursive: true });
1082
- const data = {
1083
- message,
1084
- timestamp: Date.now(),
1085
- repoPath
1086
- };
1087
- const path = cachePath(repoPath);
1088
- debug("saveCachedCommit: saving to %s", path);
1089
- await writeFile(path, JSON.stringify(data, null, 2), "utf8");
1090
- }
1091
- async function loadCachedCommit(repoPath) {
1092
- const path = cachePath(repoPath);
1093
- debug("loadCachedCommit: loading from %s", path);
1094
- try {
1095
- const raw = await readFile(path, "utf8");
1096
- const data = JSON.parse(raw);
1097
- debug("loadCachedCommit: found message from %s", new Date(data.timestamp).toISOString());
1098
- return data;
1099
- } catch {
1100
- debug("loadCachedCommit: no cached commit found");
1101
- return null;
1102
- }
1103
- }
1104
- //#endregion
1105
818
  //#region src/services/checks.ts
1106
819
  /** Config file names, checked in priority order (matches lint-staged naming conventions) */
1107
820
  const CONFIG_FILES = [
@@ -1515,19 +1228,61 @@ const statusLabel = (status) => {
1515
1228
  default: return dim(status);
1516
1229
  }
1517
1230
  };
1518
- /** Display a table of changed files with status indicators */
1519
- function showChangedFilesTable(files) {
1520
- if (files.length === 0) return;
1521
- const lines = files.map((f) => ` ${statusLabel(f.status)} ${f.path}`);
1522
- p.note(lines.join("\n"), `${files.length} file${files.length !== 1 ? "s" : ""} changed`);
1523
- }
1524
- /** Display a compact grouping summary (only shown when >1 group) */
1525
- function showGroupingSummary(groups) {
1526
- if (groups.length <= 1) return;
1527
- const lines = groups.map((g) => `${bold(g.name)} ${dim("—")} ${g.files.length} file${g.files.length !== 1 ? "s" : ""}`);
1231
+ /** Display combined view: files with status indicators grouped by commit group */
1232
+ function showGroupedFiles(groups, changedFiles) {
1233
+ const statusMap = new Map(changedFiles.map((f) => [f.path, f.status]));
1234
+ const lines = [];
1235
+ for (let i = 0; i < groups.length; i++) {
1236
+ const group = groups[i];
1237
+ lines.push(`${bold(group.name)} ${dim("—")} ${group.files.length} file${group.files.length !== 1 ? "s" : ""}`);
1238
+ for (const file of group.files) {
1239
+ const status = statusMap.get(file) ?? "M";
1240
+ lines.push(` ${statusLabel(status)} ${file}`);
1241
+ }
1242
+ if (i < groups.length - 1) lines.push("");
1243
+ }
1528
1244
  p.note(lines.join("\n"), "Commit groups");
1529
1245
  }
1530
1246
  //#endregion
1247
+ //#region src/services/clipboard.ts
1248
+ async function copyToClipboard(content) {
1249
+ for (const [cmd, args] of [
1250
+ ["wl-copy", []],
1251
+ ["xclip", ["-selection", "clipboard"]],
1252
+ ["xsel", ["--clipboard", "--input"]],
1253
+ ["pbcopy", []]
1254
+ ]) try {
1255
+ if (await new Promise((resolve) => {
1256
+ const child = spawn(cmd, args, { stdio: [
1257
+ "pipe",
1258
+ "ignore",
1259
+ "ignore"
1260
+ ] });
1261
+ let settled = false;
1262
+ const done = (result) => {
1263
+ if (settled) return;
1264
+ settled = true;
1265
+ resolve(result);
1266
+ };
1267
+ child.on("error", () => done(false));
1268
+ child.on("exit", (code) => {
1269
+ if (code !== 0) done(false);
1270
+ });
1271
+ child.stdin.write(content, (err) => {
1272
+ if (err) {
1273
+ done(false);
1274
+ return;
1275
+ }
1276
+ child.stdin.end(() => {
1277
+ child.unref();
1278
+ done(true);
1279
+ });
1280
+ });
1281
+ })) return true;
1282
+ } catch {}
1283
+ return false;
1284
+ }
1285
+ //#endregion
1531
1286
  //#region src/ui/menu.ts
1532
1287
  async function showStagingMenu(files, hasChecks) {
1533
1288
  debug("showStagingMenu: %d files", files.length);
@@ -1825,8 +1580,7 @@ async function runAutoGroupFlow(changedFiles, flags) {
1825
1580
  s.start("Analyzing files...");
1826
1581
  const validatedGroups = validateGroups((await generateGroups(included, await getProviderApiKey(provider), getModelForProvider(config, provider, PROVIDER_CONFIGS[provider].defaultModel), config.timeout ? parseInt(config.timeout, 10) : void 0, provider, config.proxy)).groups, included);
1827
1582
  s.stop("Files analyzed");
1828
- showChangedFilesTable(included);
1829
- showGroupingSummary(validatedGroups);
1583
+ showGroupedFiles(validatedGroups, included);
1830
1584
  if (flags.auto) debug("Auto mode: skipping grouping confirmation");
1831
1585
  else if (!await showGroupingConfirmation(validatedGroups, excluded)) {
1832
1586
  outro(dim("Cancelled."));
@@ -2380,12 +2134,6 @@ cli({
2380
2134
  description: "Add context hint for AI commit message generation",
2381
2135
  alias: "H"
2382
2136
  },
2383
- review: {
2384
- type: Boolean,
2385
- description: "Review staged changes with a coding model",
2386
- alias: "R",
2387
- default: false
2388
- },
2389
2137
  debug: {
2390
2138
  type: Boolean,
2391
2139
  description: "Enable debug output",
@@ -2404,8 +2152,7 @@ cli({
2404
2152
  })]
2405
2153
  }, (argv) => {
2406
2154
  setDebug(argv.flags.debug);
2407
- if (argv.flags.review) reviewCommand();
2408
- else commitCommand(argv.flags);
2155
+ commitCommand(argv.flags);
2409
2156
  });
2410
2157
  //#endregion
2411
2158
  export {};