@kud/ai-conventional-commit-cli 0.2.1 → 0.4.0

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/dist/index.js CHANGED
@@ -4,7 +4,7 @@
4
4
  import { Cli, Command, Option } from "clipanion";
5
5
 
6
6
  // src/workflow/generate.ts
7
- import chalk from "chalk";
7
+ import chalk2 from "chalk";
8
8
  import ora from "ora";
9
9
 
10
10
  // src/git.ts
@@ -89,6 +89,17 @@ var createCommit = async (title, body) => {
89
89
  await git.commit(title);
90
90
  }
91
91
  };
92
+ var resetIndex = async () => {
93
+ await git.reset(["--mixed"]);
94
+ };
95
+ var stageFiles = async (files) => {
96
+ if (!files.length) return;
97
+ await git.add(files);
98
+ };
99
+ var getStagedFiles = async () => {
100
+ const status = await git.status();
101
+ return status.staged;
102
+ };
92
103
 
93
104
  // src/style.ts
94
105
  var buildStyleProfile = (messages) => {
@@ -171,7 +182,7 @@ var buildGenerationMessages = (opts) => {
171
182
  );
172
183
  specLines.push("Locale: en");
173
184
  specLines.push(
174
- 'Output JSON Schema: { "commits": [ { "title": string, "body": string, "score": 0-100, "reasons": string[] } ], "meta": { "splitRecommended": boolean } }'
185
+ 'Output JSON Schema: { "commits": [ { "title": string, "body": string, "score": 0-100, "reasons": string[], "files"?: string[] } ], "meta": { "splitRecommended": boolean } }'
175
186
  );
176
187
  specLines.push("Primary Output Field: commits[ ].title");
177
188
  specLines.push("Title Format: <type>(<optional-scope>): <subject>");
@@ -196,7 +207,7 @@ var buildGenerationMessages = (opts) => {
196
207
  "Provide reasons array citing concrete diff elements: filenames, functions, tests, metrics."
197
208
  );
198
209
  specLines.push(
199
- `Reasoning Depth: ${config.reasoning || "low"} (low=minimal concise reasons, medium=balanced detail, high=very detailed). Adjust reasons verbosity accordingly.`
210
+ 'When mode is split, WHERE POSSIBLE add a "files" array per commit listing the most relevant changed file paths (1-6, minimize overlap across commits).'
200
211
  );
201
212
  specLines.push("Return ONLY the JSON object. No surrounding text or markdown.");
202
213
  specLines.push("Do not add fields not listed in schema.");
@@ -352,7 +363,8 @@ var CommitSchema = z.object({
352
363
  title: z.string().min(5).max(150),
353
364
  body: z.string().optional().default(""),
354
365
  score: z.number().min(0).max(100),
355
- reasons: z.array(z.string()).optional().default([])
366
+ reasons: z.array(z.string()).optional().default([]),
367
+ files: z.array(z.string()).optional().default([])
356
368
  });
357
369
  var PlanSchema = z.object({
358
370
  commits: z.array(CommitSchema).min(1),
@@ -547,7 +559,149 @@ var formatCommitTitle = (raw, opts) => {
547
559
  import { writeFileSync, mkdirSync, existsSync } from "fs";
548
560
  import { join } from "path";
549
561
  import inquirer from "inquirer";
562
+
563
+ // src/workflow/ui.ts
564
+ import chalk from "chalk";
565
+ function animateHeaderBase(text = "ai-conventional-commit") {
566
+ if (!process.stdout.isTTY || process.env.AICC_NO_ANIMATION) {
567
+ console.log("\n\u250C " + chalk.bold(text));
568
+ return Promise.resolve();
569
+ }
570
+ const palette = [
571
+ "#3a0d6d",
572
+ "#5a1ea3",
573
+ "#7a32d6",
574
+ "#9a4dff",
575
+ "#b267ff",
576
+ "#c37dff",
577
+ "#b267ff",
578
+ "#9a4dff",
579
+ "#7a32d6",
580
+ "#5a1ea3"
581
+ ];
582
+ process.stdout.write("\n");
583
+ return palette.reduce(async (p, color) => {
584
+ await p;
585
+ const frame = chalk.bold.hex(color)(text);
586
+ process.stdout.write("\r\u250C " + frame);
587
+ await new Promise((r) => setTimeout(r, 60));
588
+ }, Promise.resolve()).then(() => process.stdout.write("\n"));
589
+ }
590
+ function borderLine(content) {
591
+ if (!content) console.log("\u2502");
592
+ else console.log("\u2502 " + content);
593
+ }
594
+ function sectionTitle(label) {
595
+ console.log("\u2299 " + chalk.bold(label));
596
+ }
597
+ function abortMessage() {
598
+ console.log("\u2514 \u{1F645}\u200D\u2640\uFE0F No commit created.");
599
+ }
600
+ function finalSuccess(opts) {
601
+ const elapsedMs = Date.now() - opts.startedAt;
602
+ const seconds = elapsedMs / 1e3;
603
+ const dur = seconds >= 0.1 ? seconds.toFixed(1) + "s" : elapsedMs + "ms";
604
+ const plural = opts.count !== 1;
605
+ if (plural) console.log(`\u2514 \u2728 ${opts.count} commits created in ${dur}.`);
606
+ else console.log(`\u2514 \u2728 commit created in ${dur}.`);
607
+ }
608
+ function createPhasedSpinner(oraLib) {
609
+ const useAnim = process.stdout.isTTY && !process.env.AICC_NO_ANIMATION && !process.env.AICC_NO_SPINNER_ANIM;
610
+ const palette = [
611
+ "#3a0d6d",
612
+ "#5a1ea3",
613
+ "#7a32d6",
614
+ "#9a4dff",
615
+ "#b267ff",
616
+ "#c37dff",
617
+ "#b267ff",
618
+ "#9a4dff",
619
+ "#7a32d6",
620
+ "#5a1ea3"
621
+ ];
622
+ let label = "Starting";
623
+ let i = 0;
624
+ const spinner = oraLib({ text: chalk.bold(label), spinner: "dots" }).start();
625
+ let interval = null;
626
+ function frame() {
627
+ if (!useAnim) return;
628
+ spinner.text = chalk.bold.hex(palette[i])(label);
629
+ i = (i + 1) % palette.length;
630
+ }
631
+ if (useAnim) {
632
+ frame();
633
+ interval = setInterval(frame, 80);
634
+ }
635
+ function setLabel(next) {
636
+ label = next;
637
+ if (useAnim) {
638
+ i = 0;
639
+ frame();
640
+ } else {
641
+ spinner.text = chalk.bold(label);
642
+ }
643
+ }
644
+ function stopAnim() {
645
+ if (interval) {
646
+ clearInterval(interval);
647
+ interval = null;
648
+ }
649
+ }
650
+ return {
651
+ spinner,
652
+ async step(l, fn) {
653
+ setLabel(l);
654
+ try {
655
+ return await fn();
656
+ } catch (e) {
657
+ stopAnim();
658
+ const msg = `${l} failed: ${e?.message || e}`.replace(/^\s+/, "");
659
+ spinner.fail(msg);
660
+ throw e;
661
+ }
662
+ },
663
+ phase(l) {
664
+ setLabel(l);
665
+ },
666
+ stop() {
667
+ stopAnim();
668
+ spinner.stop();
669
+ }
670
+ };
671
+ }
672
+ function renderCommitBlock(opts) {
673
+ const dim = (s) => chalk.dim(s);
674
+ const white = (s) => chalk.white(s);
675
+ const msgColor = opts.messageLabelColor || dim;
676
+ const descColor = opts.descriptionLabelColor || dim;
677
+ const titleColor = opts.titleColor || white;
678
+ const bodyFirst = opts.bodyFirstLineColor || white;
679
+ const bodyRest = opts.bodyLineColor || white;
680
+ if (opts.fancy) {
681
+ const heading = opts.heading ? chalk.hex("#9a4dff").bold(opts.heading) : void 0;
682
+ if (heading) borderLine(heading);
683
+ borderLine(msgColor("Title:") + " " + titleColor(`${opts.indexPrefix || ""}${opts.title}`));
684
+ } else {
685
+ if (opts.heading) borderLine(chalk.bold(opts.heading));
686
+ if (!opts.hideMessageLabel)
687
+ borderLine(msgColor("Message:") + " " + titleColor(`${opts.indexPrefix || ""}${opts.title}`));
688
+ else
689
+ borderLine(msgColor("Title:") + " " + titleColor(`${opts.indexPrefix || ""}${opts.title}`));
690
+ }
691
+ borderLine();
692
+ if (opts.body) {
693
+ const lines = opts.body.split("\n");
694
+ lines.forEach((line, i) => {
695
+ if (line.trim().length === 0) borderLine();
696
+ else if (i === 0) borderLine(descColor("Description:") + " " + bodyFirst(line));
697
+ else borderLine(bodyRest(line));
698
+ });
699
+ }
700
+ }
701
+
702
+ // src/workflow/generate.ts
550
703
  async function runGenerate(config) {
704
+ const startedAt = Date.now();
551
705
  if (!await ensureStagedChanges()) {
552
706
  console.log("No staged changes.");
553
707
  return;
@@ -557,24 +711,18 @@ async function runGenerate(config) {
557
711
  console.log("No diff content detected after staging. Aborting.");
558
712
  return;
559
713
  }
560
- console.log("\n\u250C " + chalk.bold("ai-conventional-commit"));
561
- console.log("\u2502");
562
- console.log(`\u25C6 ${chalk.bold(`Detected ${files.length} staged files:`)}`);
563
- for (const f of files) console.log(" " + f.file);
564
- console.log("\u2502");
565
- const spinner = ora({ text: " Starting", spinner: "dots" }).start();
566
- function setPhase(label) {
567
- spinner.text = " " + chalk.bold(label);
568
- }
569
- async function runStep(label, fn) {
570
- setPhase(label);
571
- try {
572
- return await fn();
573
- } catch (e) {
574
- spinner.fail(`\u25C7 ${label} failed: ${e.message}`);
575
- throw e;
576
- }
714
+ if (process.stdout.isTTY) {
715
+ await animateHeaderBase();
716
+ borderLine();
577
717
  }
718
+ sectionTitle("Files");
719
+ borderLine(
720
+ chalk2.dim(`Detected ${files.length} staged ${files.length === 1 ? "file" : "files"}:`)
721
+ );
722
+ files.forEach((f) => borderLine("\u2022 " + f.file));
723
+ borderLine();
724
+ const phased = createPhasedSpinner(ora);
725
+ const runStep = (label, fn) => phased.step(label, fn);
578
726
  let style;
579
727
  let plugins;
580
728
  let messages;
@@ -600,8 +748,9 @@ async function runGenerate(config) {
600
748
  "Analyzing changes",
601
749
  async () => applyTransforms(plan.commits, plugins, { cwd: process.cwd(), env: process.env })
602
750
  );
603
- setPhase("Result found");
604
- spinner.stopAndPersist({ symbol: "\u25C6", text: " " + chalk.bold("Result found:") });
751
+ phased.phase("Suggested commit");
752
+ phased.stop();
753
+ sectionTitle("Suggested commit");
605
754
  candidates = candidates.map((c) => ({
606
755
  ...c,
607
756
  title: formatCommitTitle(c.title, {
@@ -610,15 +759,7 @@ async function runGenerate(config) {
610
759
  })
611
760
  }));
612
761
  const chosen = candidates[0];
613
- console.log(" " + chalk.yellow(chosen.title));
614
- if (chosen.body) {
615
- const indent = " ";
616
- chosen.body.split("\n").forEach((line) => {
617
- if (line.trim().length === 0) console.log(indent);
618
- else console.log(indent + chalk.gray(line));
619
- });
620
- }
621
- console.log("\u2502");
762
+ renderCommitBlock({ title: chosen.title, body: chosen.body });
622
763
  const pluginErrors = await runValidations(chosen, plugins, {
623
764
  cwd: process.cwd(),
624
765
  env: process.env
@@ -626,18 +767,22 @@ async function runGenerate(config) {
626
767
  const guardErrors = checkCandidate(chosen);
627
768
  const errors = [...pluginErrors, ...guardErrors];
628
769
  if (errors.length) {
629
- console.log(chalk.red("! Validation issues:"));
630
- errors.forEach((e) => console.log(" -", e));
631
- console.log("\u2502");
770
+ borderLine();
771
+ console.log("\u2299 " + chalk2.bold("Checks"));
772
+ const errorLines = ["Validation issues:", ...errors.map((e) => chalk2.red("\u2022 " + e))];
773
+ errorLines.forEach((l) => borderLine(l));
632
774
  }
775
+ borderLine();
633
776
  const yn = await selectYesNo();
634
777
  if (!yn) {
635
- console.log("Aborted.");
778
+ borderLine();
779
+ abortMessage();
636
780
  return;
637
781
  }
638
782
  await createCommit(chosen.title, chosen.body);
639
783
  saveSession({ plan, chosen, mode: "single" });
640
- console.log(chalk.green("Commit created."));
784
+ borderLine();
785
+ finalSuccess({ count: 1, startedAt });
641
786
  }
642
787
  function saveSession(data) {
643
788
  const dir = ".git/.aicc-cache";
@@ -649,10 +794,10 @@ async function selectYesNo() {
649
794
  {
650
795
  type: "list",
651
796
  name: "choice",
652
- message: " Use this commit message?",
797
+ message: "Use the commit?",
653
798
  choices: [
654
- { name: "\u25CF Yes", value: true },
655
- { name: "\u25CB No", value: false }
799
+ { name: "Yes", value: true },
800
+ { name: "No", value: false }
656
801
  ],
657
802
  default: 0
658
803
  }
@@ -661,7 +806,8 @@ async function selectYesNo() {
661
806
  }
662
807
 
663
808
  // src/workflow/split.ts
664
- import chalk2 from "chalk";
809
+ import chalk3 from "chalk";
810
+ import ora2 from "ora";
665
811
 
666
812
  // src/cluster.ts
667
813
  var topLevel = (file) => file.split("/")[0] || file;
@@ -707,6 +853,7 @@ import { writeFileSync as writeFileSync2, mkdirSync as mkdirSync2, existsSync as
707
853
  import { join as join2 } from "path";
708
854
  import inquirer2 from "inquirer";
709
855
  async function runSplit(config, desired) {
856
+ const startedAt = Date.now();
710
857
  if (!await ensureStagedChanges()) {
711
858
  console.log("No staged changes.");
712
859
  return;
@@ -716,43 +863,45 @@ async function runSplit(config, desired) {
716
863
  console.log("No diff content detected after staging. Aborting.");
717
864
  return;
718
865
  }
719
- console.log("\u25C7 Clustering changes");
720
- clusterHunks(files);
721
- console.log("\u25C7 Profiling style");
722
- const history = await getRecentCommitMessages(config.styleSamples);
723
- const style = buildStyleProfile(history);
724
- console.log("\u25C7 Loading plugins");
725
- const plugins = await loadPlugins(config);
726
- console.log("\u25C7 Building prompt");
727
- const messages = buildGenerationMessages({
728
- files,
729
- style,
730
- config,
731
- mode: "split",
732
- desiredCommits: desired
733
- });
734
- const provider = new OpenCodeProvider(config.model);
735
- console.log("\u25C7 Calling model for split plan");
736
- let raw;
737
- try {
738
- raw = await provider.chat(messages, { maxTokens: config.maxTokens });
739
- } catch (e) {
740
- console.log(e.message);
741
- return;
742
- }
743
- console.log("\u25C7 Parsing response");
744
- let plan;
745
- try {
746
- plan = extractJSON(raw);
747
- } catch (e) {
748
- console.log("JSON parse error: " + e.message);
749
- return;
866
+ if (process.stdout.isTTY) {
867
+ await animateHeaderBase();
868
+ borderLine();
750
869
  }
751
- console.log("\u25C7 Plan received");
752
- let candidates = await applyTransforms(plan.commits, plugins, {
753
- cwd: process.cwd(),
754
- env: process.env
870
+ sectionTitle("Files");
871
+ borderLine(
872
+ chalk3.dim(`Detected ${files.length} staged ${files.length === 1 ? "file" : "files"}:`)
873
+ );
874
+ files.forEach((f) => borderLine("\u2022 " + f.file));
875
+ borderLine();
876
+ const phased = createPhasedSpinner(ora2);
877
+ const runStep = (label, fn) => phased.step(label, fn);
878
+ await runStep("Clustering changes", async () => {
879
+ clusterHunks(files);
755
880
  });
881
+ const style = await runStep("Profiling style", async () => {
882
+ const history = await getRecentCommitMessages(config.styleSamples);
883
+ return buildStyleProfile(history);
884
+ });
885
+ const plugins = await runStep("Loading plugins", async () => loadPlugins(config));
886
+ const messages = await runStep(
887
+ "Building prompt",
888
+ async () => buildGenerationMessages({ files, style, config, mode: "split", desiredCommits: desired })
889
+ );
890
+ const provider = new OpenCodeProvider(config.model);
891
+ const raw = await runStep(
892
+ "Calling model",
893
+ async () => provider.chat(messages, { maxTokens: config.maxTokens })
894
+ );
895
+ const plan = await runStep("Parsing response", async () => extractJSON(raw));
896
+ let candidates = await runStep(
897
+ "Analyzing changes",
898
+ async () => applyTransforms(plan.commits, plugins, { cwd: process.cwd(), env: process.env })
899
+ );
900
+ const plural = candidates.length !== 1;
901
+ phased.phase(plural ? "Suggested commits" : "Suggested commit");
902
+ phased.stop();
903
+ sectionTitle(plural ? "Suggested commits" : "Suggested commit");
904
+ borderLine();
756
905
  candidates = candidates.map((c) => ({
757
906
  ...c,
758
907
  title: formatCommitTitle(c.title, {
@@ -760,48 +909,67 @@ async function runSplit(config, desired) {
760
909
  mode: config.gitmojiMode || "standard"
761
910
  })
762
911
  }));
763
- console.log(chalk2.cyan("\nProposed commits:"));
764
- candidates.forEach((c) => {
765
- console.log(chalk2.yellow(`\u2022 ${c.title}`));
766
- if (c.body) {
767
- const indent = " ";
768
- c.body.split("\n").forEach((line) => {
769
- if (line.trim().length === 0) console.log(indent);
770
- else console.log(indent + chalk2.gray(line));
771
- });
912
+ const fancy = candidates.length > 1;
913
+ candidates.forEach((c, idx) => {
914
+ renderCommitBlock({
915
+ title: c.title,
916
+ body: c.body,
917
+ heading: fancy ? `Commit n\xB0${idx + 1}` : void 0,
918
+ hideMessageLabel: fancy,
919
+ fancy
920
+ });
921
+ if (idx < candidates.length - 1) {
922
+ borderLine();
923
+ borderLine();
772
924
  }
773
925
  });
926
+ borderLine();
774
927
  const { ok } = await inquirer2.prompt([
775
928
  {
776
929
  type: "list",
777
930
  name: "ok",
778
- message: "Apply these commit messages?",
931
+ message: "Use the commits?",
779
932
  choices: [
780
- { name: "\u25CF Yes", value: true },
781
- { name: "\u25CB No", value: false }
933
+ { name: "Yes", value: true },
934
+ { name: "No", value: false }
782
935
  ],
783
936
  default: 0
784
937
  }
785
938
  ]);
786
939
  if (!ok) {
787
- console.log("Aborted.");
940
+ borderLine();
941
+ abortMessage();
788
942
  return;
789
943
  }
944
+ const allChangedFiles = [...new Set(files.map((f) => f.file))];
945
+ let useFiles = false;
946
+ if (candidates.every((c) => Array.isArray(c.files) && c.files.length > 0)) {
947
+ const flat = candidates.flatMap((c) => c.files);
948
+ const unique = new Set(flat);
949
+ if ([...unique].every((f) => allChangedFiles.includes(f))) {
950
+ useFiles = true;
951
+ }
952
+ }
953
+ if (!useFiles) {
954
+ const buckets = candidates.map(() => []);
955
+ allChangedFiles.forEach((f, i) => buckets[i % buckets.length].push(f));
956
+ candidates = candidates.map((c, i) => ({ ...c, files: buckets[i] }));
957
+ useFiles = true;
958
+ }
959
+ let success = 0;
790
960
  for (const candidate of candidates) {
791
- const pluginErrors = await runValidations(candidate, plugins, {
792
- cwd: process.cwd(),
793
- env: process.env
794
- });
795
- const guardErrors = checkCandidate(candidate);
796
- const errors = [...pluginErrors, ...guardErrors];
797
- if (errors.length) {
798
- console.log(chalk2.red("Skipping commit due to errors:"), candidate.title);
799
- errors.forEach((e) => console.log(" -", e));
800
- continue;
961
+ await resetIndex();
962
+ await stageFiles(candidate.files || []);
963
+ const stagedNow = await getStagedFiles();
964
+ if (!stagedNow.length) continue;
965
+ try {
966
+ await createCommit(candidate.title, candidate.body);
967
+ success++;
968
+ } catch (e) {
801
969
  }
802
- await createCommit(candidate.title, candidate.body);
803
- console.log(chalk2.green("Committed: ") + candidate.title);
804
970
  }
971
+ borderLine();
972
+ finalSuccess({ count: success, startedAt });
805
973
  saveSession2({ plan, chosen: candidates, mode: "split" });
806
974
  }
807
975
  function saveSession2(data) {
@@ -811,9 +979,10 @@ function saveSession2(data) {
811
979
  }
812
980
 
813
981
  // src/workflow/refine.ts
814
- import chalk3 from "chalk";
815
- import ora2 from "ora";
816
- import { readFileSync, existsSync as existsSync3 } from "fs";
982
+ import chalk4 from "chalk";
983
+ import ora3 from "ora";
984
+ import inquirer3 from "inquirer";
985
+ import { readFileSync, existsSync as existsSync3, writeFileSync as writeFileSync3, mkdirSync as mkdirSync3 } from "fs";
817
986
  import { join as join3 } from "path";
818
987
 
819
988
  // src/workflow/util.ts
@@ -839,20 +1008,39 @@ function loadSession() {
839
1008
  return null;
840
1009
  }
841
1010
  }
1011
+ function saveSession3(session) {
1012
+ const dir = ".git/.aicc-cache";
1013
+ if (!existsSync3(dir)) mkdirSync3(dir, { recursive: true });
1014
+ writeFileSync3(join3(dir, "last-session.json"), JSON.stringify(session, null, 2));
1015
+ }
842
1016
  async function runRefine(config, options) {
843
- const spinner = ora2("Loading last session").start();
1017
+ const startedAt = Date.now();
844
1018
  const session = loadSession();
845
1019
  if (!session) {
846
- spinner.fail("No previous session found.");
1020
+ console.log("No previous session found.");
847
1021
  return;
848
1022
  }
849
- spinner.succeed("Session loaded");
850
1023
  const plan = session.plan;
851
- const index = options.index ?? 0;
1024
+ const index = options.index ? Number(options.index) - 1 : 0;
852
1025
  if (!plan.commits[index]) {
853
1026
  console.log("Invalid index.");
854
1027
  return;
855
1028
  }
1029
+ if (process.stdout.isTTY) {
1030
+ await animateHeaderBase();
1031
+ borderLine();
1032
+ }
1033
+ sectionTitle("Original");
1034
+ const original = plan.commits[index];
1035
+ const originalLines = [chalk4.yellow(original.title)];
1036
+ if (original.body) {
1037
+ original.body.split("\n").forEach((line) => {
1038
+ if (line.trim().length === 0) originalLines.push("");
1039
+ else originalLines.push(chalk4.white(line));
1040
+ });
1041
+ }
1042
+ originalLines.forEach((l) => l.trim().length === 0 ? borderLine() : borderLine(l));
1043
+ borderLine();
856
1044
  const instructions = [];
857
1045
  if (options.shorter) instructions.push("Make the title shorter but keep meaning.");
858
1046
  if (options.longer) instructions.push("Add more specificity to the title.");
@@ -866,43 +1054,52 @@ async function runRefine(config, options) {
866
1054
  return;
867
1055
  }
868
1056
  }
1057
+ const phased = createPhasedSpinner(ora3);
1058
+ const runStep = (label, fn) => phased.step(label, fn);
869
1059
  const provider = new OpenCodeProvider(config.model);
870
- const messages = buildRefineMessages({
871
- originalPlan: plan,
872
- index,
873
- instructions,
874
- config
1060
+ const messages = await runStep(
1061
+ "Building prompt",
1062
+ async () => buildRefineMessages({ originalPlan: plan, index, instructions, config })
1063
+ );
1064
+ const raw = await runStep(
1065
+ "Calling model",
1066
+ async () => provider.chat(messages, { maxTokens: config.maxTokens })
1067
+ );
1068
+ const refinedPlan = await runStep("Parsing response", async () => extractJSON(raw));
1069
+ refinedPlan.commits[0].title = formatCommitTitle(refinedPlan.commits[0].title, {
1070
+ allowGitmoji: !!config.gitmoji || !!options.emoji,
1071
+ mode: config.gitmojiMode || "standard"
875
1072
  });
876
- const raw = await provider.chat(messages, { maxTokens: config.maxTokens });
877
- let refined;
878
- try {
879
- refined = extractJSON(raw);
880
- } catch (e) {
881
- console.error("Failed to parse refine response:", e.message);
1073
+ phased.phase("Suggested commit");
1074
+ phased.stop();
1075
+ sectionTitle("Suggested commit");
1076
+ renderCommitBlock({
1077
+ title: refinedPlan.commits[0].title,
1078
+ body: refinedPlan.commits[0].body,
1079
+ titleColor: (s) => chalk4.yellow(s)
1080
+ });
1081
+ borderLine();
1082
+ const { ok } = await inquirer3.prompt([
1083
+ {
1084
+ type: "list",
1085
+ name: "ok",
1086
+ message: "Use the commit?",
1087
+ choices: [
1088
+ { name: "Yes", value: true },
1089
+ { name: "No", value: false }
1090
+ ],
1091
+ default: 0
1092
+ }
1093
+ ]);
1094
+ if (!ok) {
1095
+ borderLine();
1096
+ abortMessage();
882
1097
  return;
883
1098
  }
884
- {
885
- refined.commits[0].title = formatCommitTitle(refined.commits[0].title, {
886
- allowGitmoji: !!config.gitmoji || !!options.emoji,
887
- mode: config.gitmojiMode || "standard"
888
- });
889
- }
890
- console.log(chalk3.cyan("\nRefined candidate:"));
891
- console.log(chalk3.yellow(refined.commits[0].title));
892
- if (refined.commits[0].body) {
893
- const indent = " ";
894
- refined.commits[0].body.split("\n").forEach((line) => {
895
- if (line.trim().length === 0) console.log(indent);
896
- else console.log(indent + chalk3.gray(line));
897
- });
898
- }
899
- const accept = await prompt("Accept refined version? (Y/n) ", "y");
900
- if (!/^n/i.test(accept)) {
901
- plan.commits[index] = refined.commits[0];
902
- console.log(chalk3.green("Refinement stored (not retro-committed)."));
903
- } else {
904
- console.log("Refinement discarded.");
905
- }
1099
+ session.plan.commits[index] = refinedPlan.commits[0];
1100
+ saveSession3(session);
1101
+ borderLine();
1102
+ finalSuccess({ count: 1, startedAt });
906
1103
  }
907
1104
 
908
1105
  // src/config.ts
@@ -918,8 +1115,7 @@ var DEFAULTS = {
918
1115
  maxTokens: parseInt(process.env.AICC_MAX_TOKENS || "512", 10),
919
1116
  cacheDir: ".git/.aicc-cache",
920
1117
  plugins: [],
921
- verbose: process.env.AICC_VERBOSE === "true",
922
- reasoning: process.env.AICC_REASONING || "low"
1118
+ verbose: process.env.AICC_VERBOSE === "true"
923
1119
  };
924
1120
  async function loadConfig(cwd = process.cwd()) {
925
1121
  const explorer = cosmiconfig("aicc");
@@ -935,53 +1131,160 @@ async function loadConfig(cwd = process.cwd()) {
935
1131
  return cfg;
936
1132
  }
937
1133
 
1134
+ // package.json
1135
+ var package_default = {
1136
+ name: "@kud/ai-conventional-commit-cli",
1137
+ version: "0.4.0",
1138
+ type: "module",
1139
+ description: "Opinionated, style-aware AI assistant for crafting and splitting git commits (opencode-based, provider-agnostic).",
1140
+ bin: {
1141
+ "ai-conventional-commit": "dist/index.js"
1142
+ },
1143
+ files: [
1144
+ "dist",
1145
+ "README.md",
1146
+ "LICENSE"
1147
+ ],
1148
+ scripts: {
1149
+ dev: "tsx src/index.ts",
1150
+ build: "tsup src/index.ts --format esm --dts",
1151
+ prepublishOnly: "npm run build",
1152
+ lint: "eslint .",
1153
+ format: "prettier --write .",
1154
+ test: "vitest run",
1155
+ "test:watch": "vitest",
1156
+ commit: "cz"
1157
+ },
1158
+ dependencies: {
1159
+ "@commitlint/config-conventional": "^19.8.1",
1160
+ chalk: "^5.6.0",
1161
+ clipanion: "^3.2.1",
1162
+ cosmiconfig: "^9.0.0",
1163
+ execa: "^9.6.0",
1164
+ "fast-glob": "^3.3.3",
1165
+ inquirer: "^9.3.7",
1166
+ keyv: "^5.5.0",
1167
+ "lru-cache": "^11.2.1",
1168
+ ora: "^8.2.0",
1169
+ pathe: "^2.0.3",
1170
+ "simple-git": "^3.28.0",
1171
+ "strip-ansi": "^7.1.0",
1172
+ zod: "^4.1.5"
1173
+ },
1174
+ devDependencies: {
1175
+ "@types/inquirer": "^9.0.7",
1176
+ "@types/node": "^24.3.1",
1177
+ "@typescript-eslint/eslint-plugin": "^8.42.0",
1178
+ "@typescript-eslint/parser": "^8.42.0",
1179
+ "cz-conventional-changelog": "^3.3.0",
1180
+ eslint: "^9.35.0",
1181
+ prettier: "^3.6.2",
1182
+ tsup: "^8.5.0",
1183
+ tsx: "^4.20.5",
1184
+ typescript: "^5.9.2",
1185
+ vitest: "^3.2.4"
1186
+ },
1187
+ config: {
1188
+ commitizen: {
1189
+ path: "cz-conventional-changelog"
1190
+ }
1191
+ },
1192
+ engines: {
1193
+ node: ">=18.17"
1194
+ },
1195
+ license: "MIT"
1196
+ };
1197
+
938
1198
  // src/index.ts
1199
+ var pkgVersion = package_default.version || "0.0.0";
939
1200
  var GenerateCommand = class extends Command {
940
- static paths = [[`generate`], [`run`], [`commit`], []];
1201
+ static paths = [[`generate`], []];
1202
+ static usage = Command.Usage({
1203
+ description: "Generate a conventional commit message for staged changes.",
1204
+ details: `Generates a single commit message using AI with style + guardrails.
1205
+ Add --gitmoji[-pure] to enable emoji styles.`,
1206
+ examples: [
1207
+ ["Generate a commit with gitmoji style", "ai-conventional-commit generate --gitmoji"]
1208
+ ]
1209
+ });
941
1210
  gitmoji = Option.Boolean("--gitmoji", false, {
942
- description: "Gitmoji mode: emoji acts as type (emoji: subject)"
1211
+ description: "Gitmoji mode (vs --gitmoji-pure): emoji + type (emoji: subject)"
943
1212
  });
944
1213
  gitmojiPure = Option.Boolean("--gitmoji-pure", false, {
945
- description: "Pure gitmoji mode: emoji: subject (no type)"
1214
+ description: "Pure gitmoji mode (vs --gitmoji): emoji only (emoji: subject)"
1215
+ });
1216
+ model = Option.String("-m,--model", {
1217
+ required: false,
1218
+ description: "Model provider/name (e.g. github-copilot/gpt-5)"
946
1219
  });
947
- reasoning = Option.String("--reasoning", { required: false });
948
1220
  async execute() {
949
1221
  const config = await loadConfig();
950
1222
  if (this.gitmoji || this.gitmojiPure) {
951
1223
  config.gitmoji = true;
952
1224
  config.gitmojiMode = this.gitmojiPure ? "gitmoji-pure" : "gitmoji";
953
1225
  }
954
- if (this.reasoning) config.reasoning = this.reasoning;
1226
+ if (this.model) config.model = this.model;
955
1227
  await runGenerate(config);
956
1228
  }
957
1229
  };
958
1230
  var SplitCommand = class extends Command {
959
1231
  static paths = [[`split`]];
1232
+ static usage = Command.Usage({
1233
+ description: "Propose multiple smaller conventional commits for current staged diff.",
1234
+ details: `Analyzes staged changes, groups them logically and suggests multiple commit messages.
1235
+ Use --max to limit the number of proposals.`,
1236
+ examples: [
1237
+ [
1238
+ "Split into at most 3 commits with gitmoji",
1239
+ "ai-conventional-commit split --max 3 --gitmoji"
1240
+ ]
1241
+ ]
1242
+ });
960
1243
  max = Option.String("--max", { description: "Max proposed commits", required: false });
961
- gitmoji = Option.Boolean("--gitmoji", false);
962
- gitmojiPure = Option.Boolean("--gitmoji-pure", false);
963
- reasoning = Option.String("--reasoning", { required: false });
1244
+ gitmoji = Option.Boolean("--gitmoji", false, {
1245
+ description: "Gitmoji mode (vs --gitmoji-pure): emoji + type"
1246
+ });
1247
+ gitmojiPure = Option.Boolean("--gitmoji-pure", false, {
1248
+ description: "Pure gitmoji mode (vs --gitmoji): emoji only"
1249
+ });
1250
+ model = Option.String("-m,--model", {
1251
+ required: false,
1252
+ description: "Model provider/name override"
1253
+ });
964
1254
  async execute() {
965
1255
  const config = await loadConfig();
966
1256
  if (this.gitmoji || this.gitmojiPure) {
967
1257
  config.gitmoji = true;
968
1258
  config.gitmojiMode = this.gitmojiPure ? "gitmoji-pure" : "gitmoji";
969
1259
  }
970
- if (this.reasoning) config.reasoning = this.reasoning;
1260
+ if (this.model) config.model = this.model;
971
1261
  await runSplit(config, this.max ? parseInt(this.max, 10) : void 0);
972
1262
  }
973
1263
  };
974
1264
  var RefineCommand = class extends Command {
975
1265
  static paths = [[`refine`]];
976
- shorter = Option.Boolean("--shorter", false);
977
- longer = Option.Boolean("--longer", false);
978
- scope = Option.String("--scope");
979
- emoji = Option.Boolean("--emoji", false);
980
- index = Option.String("--index");
981
- reasoning = Option.String("--reasoning", { required: false });
1266
+ static usage = Command.Usage({
1267
+ description: "Refine the last (or chosen) commit message with style rules.",
1268
+ details: `Allows targeted improvements: shorter/longer length, inject scope, add emoji, or select a specific index when multiple commits were generated earlier.`,
1269
+ examples: [
1270
+ ["Shorten the last commit message", "ai-conventional-commit refine --shorter"],
1271
+ ["Add a scope to the last commit", "ai-conventional-commit refine --scope ui"]
1272
+ ]
1273
+ });
1274
+ shorter = Option.Boolean("--shorter", false, { description: "Make message more concise" });
1275
+ longer = Option.Boolean("--longer", false, { description: "Expand message with detail" });
1276
+ scope = Option.String("--scope", { description: "Override/add scope (e.g. ui, api)" });
1277
+ emoji = Option.Boolean("--emoji", false, { description: "Add appropriate gitmoji (non-pure)" });
1278
+ index = Option.String("--index", {
1279
+ description: "Select commit index if multiple were generated"
1280
+ });
1281
+ model = Option.String("-m,--model", {
1282
+ required: false,
1283
+ description: "Model provider/name override"
1284
+ });
982
1285
  async execute() {
983
1286
  const config = await loadConfig();
984
- if (this.reasoning) config.reasoning = this.reasoning;
1287
+ if (this.model) config.model = this.model;
985
1288
  await runRefine(config, {
986
1289
  shorter: this.shorter,
987
1290
  longer: this.longer,
@@ -991,14 +1294,57 @@ var RefineCommand = class extends Command {
991
1294
  });
992
1295
  }
993
1296
  };
1297
+ var HelpCommand = class extends Command {
1298
+ static paths = [[`--help`], [`-h`]];
1299
+ // capture explicit help
1300
+ async execute() {
1301
+ this.context.stdout.write(globalHelp() + "\n");
1302
+ }
1303
+ };
1304
+ function globalHelp() {
1305
+ return `ai-conventional-commit v${pkgVersion}
1306
+
1307
+ Usage:
1308
+ ai-conventional-commit [generate] [options] Generate a commit (default)
1309
+ ai-conventional-commit split [options] Propose multiple commits
1310
+ ai-conventional-commit refine [options] Refine last or indexed commit
1311
+
1312
+ Global Options:
1313
+ -m, --model <provider/name> Override model provider/name
1314
+ --gitmoji[-pure] Gitmoji modes: emoji + type (default) or pure emoji only
1315
+ -h, --help Show this help
1316
+ -V, --version Show version
1317
+
1318
+ Refine Options:
1319
+ --shorter / --longer Adjust message length
1320
+ --scope <scope> Add or replace scope
1321
+ --emoji Add suitable gitmoji
1322
+ --index <n> Select commit index
1323
+
1324
+ Examples:
1325
+ ai-conventional-commit --gitmoji
1326
+ ai-conventional-commit --gitmoji-pure
1327
+ ai-conventional-commit split --max 3 --gitmoji
1328
+ ai-conventional-commit refine --scope api --emoji
1329
+ `;
1330
+ }
1331
+ var VersionCommand = class extends Command {
1332
+ static paths = [[`--version`], [`-V`]];
1333
+ async execute() {
1334
+ this.context.stdout.write(`${pkgVersion}
1335
+ `);
1336
+ }
1337
+ };
994
1338
  var cli = new Cli({
995
1339
  binaryLabel: "ai-conventional-commit",
996
1340
  binaryName: "ai-conventional-commit",
997
- binaryVersion: "0.1.0"
1341
+ binaryVersion: pkgVersion
998
1342
  });
999
1343
  cli.register(GenerateCommand);
1000
1344
  cli.register(SplitCommand);
1001
1345
  cli.register(RefineCommand);
1346
+ cli.register(HelpCommand);
1347
+ cli.register(VersionCommand);
1002
1348
  cli.runExit(process.argv.slice(2), {
1003
1349
  stdin: process.stdin,
1004
1350
  stdout: process.stdout,