@kud/ai-conventional-commit-cli 1.0.0 → 1.1.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/README.md CHANGED
@@ -98,6 +98,9 @@ git add .
98
98
  # Generate a single commit suggestion
99
99
  ai-conventional-commit
100
100
 
101
+ # Auto-confirm without prompting (useful for automation)
102
+ ai-conventional-commit --yes
103
+
101
104
  # Propose multiple commits (interactive confirm + real selective staging)
102
105
  ai-conventional-commit split
103
106
 
@@ -160,6 +163,7 @@ Helpful flags:
160
163
 
161
164
  - `--style <standard|gitmoji|gitmoji-pure>`
162
165
  - `--model <provider/name>` (override)
166
+ - `-y, --yes` (auto-confirm without prompting)
163
167
  - `--scope <scope>` (refine)
164
168
  - `--shorter` / `--longer`
165
169
 
@@ -274,7 +278,7 @@ ai-conventional-commit models --interactive --save # pick + persist globally
274
278
  ai-conventional-commit models --current # show active model + source
275
279
  ```
276
280
 
277
- `MODEL`, `PRIVACY`, `STYLE`, `STYLE_SAMPLES`, `MAX_TOKENS`, `MAX_FILE_LINES`, `VERBOSE`, `MODEL_TIMEOUT_MS`, `DEBUG`, `PRINT_LOGS`, `DEBUG_PROVIDER=mock`.
281
+ `MODEL`, `PRIVACY`, `STYLE`, `STYLE_SAMPLES`, `MAX_TOKENS`, `MAX_FILE_LINES`, `VERBOSE`, `YES`, `MODEL_TIMEOUT_MS`, `DEBUG`, `PRINT_LOGS`, `DEBUG_PROVIDER=mock`.
278
282
 
279
283
  **Note:** `skipFilePatterns` cannot be set via environment variable - use config file or accept defaults.
280
284
 
@@ -0,0 +1,122 @@
1
+ // src/config.ts
2
+ import { cosmiconfig } from "cosmiconfig";
3
+ import { resolve, dirname, join } from "path";
4
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs";
5
+ import { homedir } from "os";
6
+ var DEFAULTS = {
7
+ model: process.env.AICC_MODEL || "github-copilot/gpt-4.1",
8
+ privacy: process.env.AICC_PRIVACY || "low",
9
+ style: process.env.AICC_STYLE || "standard",
10
+ styleSamples: parseInt(process.env.AICC_STYLE_SAMPLES || "120", 10),
11
+ maxTokens: parseInt(process.env.AICC_MAX_TOKENS || "512", 10),
12
+ maxFileLines: parseInt(process.env.AICC_MAX_FILE_LINES || "1000", 10),
13
+ skipFilePatterns: [
14
+ "**/package-lock.json",
15
+ "**/yarn.lock",
16
+ "**/pnpm-lock.yaml",
17
+ "**/bun.lockb",
18
+ "**/composer.lock",
19
+ "**/Gemfile.lock",
20
+ "**/Cargo.lock",
21
+ "**/poetry.lock",
22
+ "**/*.d.ts",
23
+ "**/dist/**",
24
+ "**/build/**",
25
+ "**/.next/**",
26
+ "**/out/**",
27
+ "**/coverage/**",
28
+ "**/*.min.js",
29
+ "**/*.min.css",
30
+ "**/*.map"
31
+ ],
32
+ cacheDir: ".git/.aicc-cache",
33
+ plugins: [],
34
+ verbose: process.env.AICC_VERBOSE === "true",
35
+ yes: process.env.AICC_YES === "true"
36
+ };
37
+ function getGlobalConfigPath() {
38
+ const base = process.env.XDG_CONFIG_HOME || join(homedir(), ".config");
39
+ return resolve(base, "ai-conventional-commit-cli", "aicc.json");
40
+ }
41
+ function saveGlobalConfig(partial) {
42
+ const filePath = getGlobalConfigPath();
43
+ const dir = dirname(filePath);
44
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
45
+ let existing = {};
46
+ if (existsSync(filePath)) {
47
+ try {
48
+ existing = JSON.parse(readFileSync(filePath, "utf8")) || {};
49
+ } catch (e) {
50
+ if (process.env.AICC_VERBOSE === "true") {
51
+ console.error("[ai-cc] Failed to parse existing global config, overwriting.");
52
+ }
53
+ }
54
+ }
55
+ const merged = { ...existing, ...partial };
56
+ writeFileSync(filePath, JSON.stringify(merged, null, 2) + "\n", "utf8");
57
+ return filePath;
58
+ }
59
+ async function loadConfig(cwd = process.cwd()) {
60
+ return (await loadConfigDetailed(cwd)).config;
61
+ }
62
+ async function loadConfigDetailed(cwd = process.cwd()) {
63
+ let globalCfg = {};
64
+ const globalPath = getGlobalConfigPath();
65
+ if (existsSync(globalPath)) {
66
+ try {
67
+ globalCfg = JSON.parse(readFileSync(globalPath, "utf8")) || {};
68
+ } catch (e) {
69
+ if (process.env.AICC_VERBOSE === "true") {
70
+ console.error("[ai-cc] Failed to parse global config, ignoring.");
71
+ }
72
+ }
73
+ }
74
+ const explorer = cosmiconfig("aicc");
75
+ const result = await explorer.search(cwd);
76
+ const projectCfg = result?.config || {};
77
+ const envCfg = {};
78
+ if (process.env.AICC_MODEL) envCfg.model = process.env.AICC_MODEL;
79
+ if (process.env.AICC_PRIVACY) envCfg.privacy = process.env.AICC_PRIVACY;
80
+ if (process.env.AICC_STYLE) envCfg.style = process.env.AICC_STYLE;
81
+ if (process.env.AICC_STYLE_SAMPLES)
82
+ envCfg.styleSamples = parseInt(process.env.AICC_STYLE_SAMPLES, 10);
83
+ if (process.env.AICC_MAX_TOKENS) envCfg.maxTokens = parseInt(process.env.AICC_MAX_TOKENS, 10);
84
+ if (process.env.AICC_MAX_FILE_LINES)
85
+ envCfg.maxFileLines = parseInt(process.env.AICC_MAX_FILE_LINES, 10);
86
+ if (process.env.AICC_VERBOSE) envCfg.verbose = process.env.AICC_VERBOSE === "true";
87
+ if (process.env.AICC_YES) envCfg.yes = process.env.AICC_YES === "true";
88
+ const merged = {
89
+ ...DEFAULTS,
90
+ ...globalCfg,
91
+ ...projectCfg,
92
+ ...envCfg
93
+ };
94
+ merged.plugins = (merged.plugins || []).filter((p) => {
95
+ const abs = resolve(cwd, p);
96
+ return existsSync(abs);
97
+ });
98
+ if (!merged.skipFilePatterns) {
99
+ merged.skipFilePatterns = DEFAULTS.skipFilePatterns;
100
+ }
101
+ const sources = Object.keys(merged).reduce((acc, key) => {
102
+ const k = key;
103
+ let src = "default";
104
+ if (k in globalCfg) src = "global";
105
+ if (k in projectCfg) src = "project";
106
+ if (k in envCfg) src = "env";
107
+ acc[k] = src;
108
+ return acc;
109
+ }, {});
110
+ const withMeta = Object.assign(merged, { _sources: sources });
111
+ return {
112
+ config: withMeta,
113
+ raw: { defaults: DEFAULTS, global: globalCfg, project: projectCfg, env: envCfg }
114
+ };
115
+ }
116
+
117
+ export {
118
+ getGlobalConfigPath,
119
+ saveGlobalConfig,
120
+ loadConfig,
121
+ loadConfigDetailed
122
+ };
@@ -0,0 +1,12 @@
1
+ import {
2
+ getGlobalConfigPath,
3
+ loadConfig,
4
+ loadConfigDetailed,
5
+ saveGlobalConfig
6
+ } from "./chunk-F3BOAVBY.js";
7
+ export {
8
+ getGlobalConfigPath,
9
+ loadConfig,
10
+ loadConfigDetailed,
11
+ saveGlobalConfig
12
+ };
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  loadConfig
4
- } from "./chunk-HJR5M6U7.js";
4
+ } from "./chunk-F3BOAVBY.js";
5
5
  import {
6
6
  OpenCodeProvider,
7
7
  abortMessage,
@@ -213,7 +213,7 @@ async function runValidations(candidate, plugins, ctx) {
213
213
  // src/workflow/generate.ts
214
214
  import { writeFileSync, mkdirSync, existsSync } from "fs";
215
215
  import { join } from "path";
216
- import inquirer from "inquirer";
216
+ import { select } from "@inquirer/prompts";
217
217
  async function runGenerate(config) {
218
218
  const startedAt = Date.now();
219
219
  if (!await ensureStagedChanges()) {
@@ -323,7 +323,7 @@ async function runGenerate(config) {
323
323
  errorLines.forEach((l) => borderLine(l));
324
324
  }
325
325
  borderLine();
326
- const yn = await selectYesNo();
326
+ const yn = config.yes || await selectYesNo();
327
327
  if (!yn) {
328
328
  borderLine();
329
329
  abortMessage();
@@ -340,14 +340,14 @@ function saveSession(data) {
340
340
  writeFileSync(join(dir, "last-session.json"), JSON.stringify(data, null, 2));
341
341
  }
342
342
  async function selectYesNo() {
343
- const { choice } = await inquirer.prompt([
344
- {
345
- type: "confirm",
346
- name: "choice",
347
- message: "Use the commit?",
348
- default: true
349
- }
350
- ]);
343
+ const choice = await select({
344
+ message: "Use the commit?",
345
+ choices: [
346
+ { name: "Yes", value: true },
347
+ { name: "No", value: false }
348
+ ],
349
+ default: true
350
+ });
351
351
  return choice;
352
352
  }
353
353
 
@@ -397,7 +397,7 @@ var clusterHunks = (files) => {
397
397
  // src/workflow/split.ts
398
398
  import { writeFileSync as writeFileSync2, mkdirSync as mkdirSync2, existsSync as existsSync2 } from "fs";
399
399
  import { join as join2 } from "path";
400
- import inquirer2 from "inquirer";
400
+ import { select as select2 } from "@inquirer/prompts";
401
401
  async function runSplit(config, desired) {
402
402
  const startedAt = Date.now();
403
403
  if (!await ensureStagedChanges()) {
@@ -506,14 +506,14 @@ async function runSplit(config, desired) {
506
506
  }
507
507
  });
508
508
  borderLine();
509
- const { ok } = await inquirer2.prompt([
510
- {
511
- type: "confirm",
512
- name: "ok",
513
- message: "Use the commits?",
514
- default: true
515
- }
516
- ]);
509
+ const ok = config.yes || await select2({
510
+ message: "Use the commits?",
511
+ choices: [
512
+ { name: "Yes", value: true },
513
+ { name: "No", value: false }
514
+ ],
515
+ default: true
516
+ });
517
517
  if (!ok) {
518
518
  borderLine();
519
519
  abortMessage();
@@ -559,7 +559,7 @@ function saveSession2(data) {
559
559
  // src/workflow/refine.ts
560
560
  import chalk3 from "chalk";
561
561
  import ora3 from "ora";
562
- import inquirer3 from "inquirer";
562
+ import inquirer from "inquirer";
563
563
  import { readFileSync, existsSync as existsSync3, writeFileSync as writeFileSync3, mkdirSync as mkdirSync3 } from "fs";
564
564
  import { join as join3 } from "path";
565
565
 
@@ -656,7 +656,7 @@ async function runRefine(config, options) {
656
656
  titleColor: (s) => chalk3.yellow(s)
657
657
  });
658
658
  borderLine();
659
- const { ok } = await inquirer3.prompt([
659
+ const ok = config.yes || (await inquirer.prompt([
660
660
  {
661
661
  type: "list",
662
662
  name: "ok",
@@ -667,7 +667,7 @@ async function runRefine(config, options) {
667
667
  ],
668
668
  default: 0
669
669
  }
670
- ]);
670
+ ])).ok;
671
671
  if (!ok) {
672
672
  borderLine();
673
673
  abortMessage();
@@ -684,7 +684,7 @@ import { readFileSync as readFileSync2 } from "fs";
684
684
  import { fileURLToPath } from "url";
685
685
  import { dirname, join as join4 } from "path";
686
686
  import { execa } from "execa";
687
- import inquirer4 from "inquirer";
687
+ import inquirer2 from "inquirer";
688
688
  var __dirname = dirname(fileURLToPath(import.meta.url));
689
689
  var pkgVersion = JSON.parse(readFileSync2(join4(__dirname, "..", "package.json"), "utf8")).version || "0.0.0";
690
690
  var RootCommand = class extends Command {
@@ -732,10 +732,14 @@ Refine Options:
732
732
  required: false,
733
733
  description: "Model provider/name (e.g. github-copilot/gpt-4.1)"
734
734
  });
735
+ yes = Option.Boolean("-y,--yes", false, {
736
+ description: "Auto-confirm commit without prompting"
737
+ });
735
738
  async execute() {
736
739
  const config = await loadConfig();
737
740
  if (this.style) config.style = this.style;
738
741
  if (this.model) config.model = this.model;
742
+ if (this.yes) config.yes = this.yes;
739
743
  await runGenerate(config);
740
744
  }
741
745
  };
@@ -761,12 +765,16 @@ var GenerateCommand = class extends Command {
761
765
  required: false,
762
766
  description: "Model provider/name (e.g. github-copilot/gpt-4.1)"
763
767
  });
768
+ yes = Option.Boolean("-y,--yes", false, {
769
+ description: "Auto-confirm commit without prompting"
770
+ });
764
771
  async execute() {
765
772
  const config = await loadConfig();
766
773
  if (this.style) {
767
774
  config.style = this.style;
768
775
  }
769
776
  if (this.model) config.model = this.model;
777
+ if (this.yes) config.yes = this.yes;
770
778
  await runGenerate(config);
771
779
  }
772
780
  };
@@ -792,10 +800,14 @@ var SplitCommand = class extends Command {
792
800
  required: false,
793
801
  description: "Model provider/name override"
794
802
  });
803
+ yes = Option.Boolean("-y,--yes", false, {
804
+ description: "Auto-confirm commit without prompting"
805
+ });
795
806
  async execute() {
796
807
  const config = await loadConfig();
797
808
  if (this.style) config.style = this.style;
798
809
  if (this.model) config.model = this.model;
810
+ if (this.yes) config.yes = this.yes;
799
811
  await runSplit(config, this.max ? parseInt(this.max, 10) : void 0);
800
812
  }
801
813
  };
@@ -823,10 +835,14 @@ var RefineCommand = class extends Command {
823
835
  required: false,
824
836
  description: "Model provider/name override"
825
837
  });
838
+ yes = Option.Boolean("-y,--yes", false, {
839
+ description: "Auto-confirm commit without prompting"
840
+ });
826
841
  async execute() {
827
842
  const config = await loadConfig();
828
843
  if (this.style) config.style = this.style;
829
844
  if (this.model) config.model = this.model;
845
+ if (this.yes) config.yes = this.yes;
830
846
  await runRefine(config, {
831
847
  shorter: this.shorter,
832
848
  longer: this.longer,
@@ -857,7 +873,7 @@ var ModelsCommand = class extends Command {
857
873
  });
858
874
  async execute() {
859
875
  if (this.current) {
860
- const { loadConfigDetailed } = await import("./config-C3S4LWLD.js");
876
+ const { loadConfigDetailed } = await import("./config-RHGCFLHQ.js");
861
877
  const { config } = await loadConfigDetailed();
862
878
  this.context.stdout.write(`${config.model} (source: ${config._sources.model})
863
879
  `);
@@ -890,7 +906,7 @@ var ModelsCommand = class extends Command {
890
906
  this.context.stdout.write(stdout.trim() + "\n");
891
907
  return;
892
908
  }
893
- const { model } = await inquirer4.prompt([
909
+ const { model } = await inquirer2.prompt([
894
910
  {
895
911
  name: "model",
896
912
  type: "list",
@@ -901,7 +917,7 @@ var ModelsCommand = class extends Command {
901
917
  this.context.stdout.write(model + "\n");
902
918
  if (this.save) {
903
919
  try {
904
- const { saveGlobalConfig } = await import("./config-C3S4LWLD.js");
920
+ const { saveGlobalConfig } = await import("./config-RHGCFLHQ.js");
905
921
  const path = saveGlobalConfig({ model });
906
922
  this.context.stdout.write(`Saved as default model in ${path}
907
923
  `);
@@ -936,7 +952,7 @@ var ConfigShowCommand = class extends Command {
936
952
  });
937
953
  json = Option.Boolean("--json", false, { description: "Output JSON including _sources" });
938
954
  async execute() {
939
- const { loadConfigDetailed } = await import("./config-C3S4LWLD.js");
955
+ const { loadConfigDetailed } = await import("./config-RHGCFLHQ.js");
940
956
  const { config, raw } = await loadConfigDetailed();
941
957
  if (this.json) {
942
958
  this.context.stdout.write(JSON.stringify({ config, raw }, null, 2) + "\n");
@@ -961,7 +977,7 @@ var ConfigGetCommand = class extends Command {
961
977
  key = Option.String();
962
978
  withSource = Option.Boolean("--with-source", false, { description: "Append source label" });
963
979
  async execute() {
964
- const { loadConfigDetailed } = await import("./config-C3S4LWLD.js");
980
+ const { loadConfigDetailed } = await import("./config-RHGCFLHQ.js");
965
981
  const { config } = await loadConfigDetailed();
966
982
  const key = this.key;
967
983
  if (!(key in config)) {
@@ -983,17 +999,18 @@ var ConfigSetCommand = class extends Command {
983
999
  static paths = [[`config`, `set`]];
984
1000
  static usage = Command.Usage({
985
1001
  description: "Set and persist a global configuration key.",
986
- details: "Writes to the global aicc.json (XDG config). Accepts JSON for complex values. Only allowed keys: model, style, privacy, styleSamples, maxTokens, verbose.",
1002
+ details: "Writes to the global aicc.json (XDG config). Accepts JSON for complex values. Only allowed keys: model, style, privacy, styleSamples, maxTokens, verbose, yes.",
987
1003
  examples: [
988
1004
  ["Set default model", "ai-conventional-commit config set model github-copilot/gpt-4.1"],
989
1005
  ["Set style to gitmoji", "ai-conventional-commit config set style gitmoji"],
990
- ["Enable verbose mode", "ai-conventional-commit config set verbose true"]
1006
+ ["Enable verbose mode", "ai-conventional-commit config set verbose true"],
1007
+ ["Auto-confirm commits", "ai-conventional-commit config set yes true"]
991
1008
  ]
992
1009
  });
993
1010
  key = Option.String();
994
1011
  value = Option.String();
995
1012
  async execute() {
996
- const allowed = /* @__PURE__ */ new Set(["model", "style", "privacy", "styleSamples", "maxTokens", "verbose"]);
1013
+ const allowed = /* @__PURE__ */ new Set(["model", "style", "privacy", "styleSamples", "maxTokens", "verbose", "yes"]);
997
1014
  if (!allowed.has(this.key)) {
998
1015
  this.context.stderr.write(`Cannot set key: ${this.key}
999
1016
  `);
@@ -1009,7 +1026,7 @@ var ConfigSetCommand = class extends Command {
1009
1026
  } catch {
1010
1027
  }
1011
1028
  }
1012
- const { saveGlobalConfig } = await import("./config-C3S4LWLD.js");
1029
+ const { saveGlobalConfig } = await import("./config-RHGCFLHQ.js");
1013
1030
  const path = saveGlobalConfig({ [this.key]: parsed });
1014
1031
  this.context.stdout.write(`Saved ${this.key} to ${path}
1015
1032
  `);
@@ -1029,11 +1046,15 @@ var RewordCommand = class extends Command {
1029
1046
  hash = Option.String({ required: false });
1030
1047
  style = Option.String("--style", { required: false, description: "Title style override" });
1031
1048
  model = Option.String("-m,--model", { required: false, description: "Model override" });
1049
+ yes = Option.Boolean("-y,--yes", false, {
1050
+ description: "Auto-confirm commit without prompting"
1051
+ });
1032
1052
  async execute() {
1033
- const { runReword } = await import("./reword-Q7MES34W.js");
1053
+ const { runReword } = await import("./reword-7GG233AE.js");
1034
1054
  const config = await loadConfig();
1035
1055
  if (this.style) config.style = this.style;
1036
1056
  if (this.model) config.model = this.model;
1057
+ if (this.yes) config.yes = this.yes;
1037
1058
  let target = this.hash;
1038
1059
  if (!target) {
1039
1060
  try {
@@ -1049,7 +1070,7 @@ var RewordCommand = class extends Command {
1049
1070
  value: c.hash
1050
1071
  }));
1051
1072
  choices.push({ name: "Cancel", value: "__CANCEL__" });
1052
- const { picked } = await inquirer4.prompt([
1073
+ const { picked } = await inquirer2.prompt([
1053
1074
  {
1054
1075
  type: "list",
1055
1076
  name: "picked",
@@ -0,0 +1,212 @@
1
+ import {
2
+ OpenCodeProvider,
3
+ abortMessage,
4
+ animateHeaderBase,
5
+ borderLine,
6
+ buildRefineMessages,
7
+ createPhasedSpinner,
8
+ extractJSON,
9
+ finalSuccess,
10
+ formatCommitTitle,
11
+ renderCommitBlock,
12
+ sectionTitle
13
+ } from "./chunk-WW3N76NL.js";
14
+
15
+ // src/workflow/reword.ts
16
+ import chalk from "chalk";
17
+ import ora from "ora";
18
+ import inquirer from "inquirer";
19
+ import { simpleGit } from "simple-git";
20
+ var git = simpleGit();
21
+ async function getCommitMessage(hash) {
22
+ try {
23
+ const raw = await git.show([`${hash}`, "--quiet", "--format=%P%n%B"]);
24
+ const lines = raw.split("\n");
25
+ const parentsLine = lines.shift() || "";
26
+ const parents = parentsLine.trim().length ? parentsLine.trim().split(/\s+/) : [];
27
+ const message = lines.join("\n").trim();
28
+ if (!message) return null;
29
+ const [first, ...rest] = message.split("\n");
30
+ const body = rest.join("\n").trim() || void 0;
31
+ return { title: first, body, parents };
32
+ } catch {
33
+ return null;
34
+ }
35
+ }
36
+ async function isAncestor(ancestor, head) {
37
+ try {
38
+ const mb = (await git.raw(["merge-base", ancestor, head])).trim();
39
+ const anc = (await git.revparse([ancestor])).trim();
40
+ return mb === anc;
41
+ } catch {
42
+ return false;
43
+ }
44
+ }
45
+ async function runReword(config, hash) {
46
+ const startedAt = Date.now();
47
+ const commit = await getCommitMessage(hash);
48
+ if (!commit) {
49
+ console.log(`Commit not found: ${hash}`);
50
+ return;
51
+ }
52
+ if (commit.parents.length > 1) {
53
+ console.log("Refusing to reword a merge commit (multiple parents).");
54
+ return;
55
+ }
56
+ if (process.stdout.isTTY) {
57
+ await animateHeaderBase("ai-conventional-commit", config.model);
58
+ borderLine();
59
+ }
60
+ sectionTitle("Original commit");
61
+ borderLine(chalk.yellow(commit.title));
62
+ if (commit.body) {
63
+ commit.body.split("\n").forEach((l) => l.trim().length ? borderLine(l) : borderLine());
64
+ }
65
+ borderLine();
66
+ const instructions = [
67
+ "Improve clarity & conformity to Conventional Commits while preserving meaning."
68
+ ];
69
+ const syntheticPlan = {
70
+ commits: [
71
+ {
72
+ title: commit.title,
73
+ body: commit.body,
74
+ score: 0,
75
+ reasons: []
76
+ }
77
+ ]
78
+ };
79
+ const provider = new OpenCodeProvider(config.model);
80
+ const phased = createPhasedSpinner(ora);
81
+ let refined = null;
82
+ try {
83
+ phased.phase("Preparing prompt");
84
+ const messages = buildRefineMessages({
85
+ originalPlan: syntheticPlan,
86
+ index: 0,
87
+ instructions,
88
+ config
89
+ });
90
+ phased.phase("Calling model");
91
+ const raw = await provider.chat(messages, { maxTokens: config.maxTokens });
92
+ phased.phase("Parsing response");
93
+ refined = await extractJSON(raw);
94
+ } catch (e) {
95
+ phased.spinner.fail("Reword failed: " + (e?.message || e));
96
+ return;
97
+ }
98
+ phased.stop();
99
+ if (!refined || !refined.commits.length) {
100
+ console.log("No refined commit produced.");
101
+ return;
102
+ }
103
+ const candidate = refined.commits[0];
104
+ candidate.title = formatCommitTitle(candidate.title, {
105
+ allowGitmoji: config.style === "gitmoji" || config.style === "gitmoji-pure",
106
+ mode: config.style
107
+ });
108
+ sectionTitle("Proposed commit");
109
+ renderCommitBlock({
110
+ title: chalk.yellow(candidate.title),
111
+ body: candidate.body,
112
+ hideMessageLabel: true
113
+ });
114
+ borderLine();
115
+ const resolvedHash = (await git.revparse([hash])).trim();
116
+ const headHash = (await git.revparse(["HEAD"])).trim();
117
+ const isHead = headHash === resolvedHash || headHash.startsWith(resolvedHash);
118
+ const ok = config.yes || (await inquirer.prompt([
119
+ {
120
+ type: "list",
121
+ name: "ok",
122
+ message: isHead ? "Amend HEAD with this message?" : "Apply rewrite (history will change)?",
123
+ choices: [
124
+ { name: "Yes", value: true },
125
+ { name: "No", value: false }
126
+ ],
127
+ default: 0
128
+ }
129
+ ])).ok;
130
+ if (!ok) {
131
+ borderLine();
132
+ abortMessage();
133
+ return;
134
+ }
135
+ const full = candidate.body ? `${candidate.title}
136
+
137
+ ${candidate.body}` : candidate.title;
138
+ if (isHead) {
139
+ try {
140
+ await git.commit(full, { "--amend": null });
141
+ } catch (e) {
142
+ borderLine("Failed to amend HEAD: " + (e?.message || e));
143
+ borderLine();
144
+ abortMessage();
145
+ return;
146
+ }
147
+ borderLine();
148
+ finalSuccess({ count: 1, startedAt });
149
+ return;
150
+ }
151
+ const ancestorOk = await isAncestor(resolvedHash, headHash);
152
+ if (!ancestorOk) {
153
+ borderLine("Selected commit is not an ancestor of HEAD.");
154
+ borderLine("Cannot safely rewrite automatically.");
155
+ borderLine();
156
+ abortMessage();
157
+ return;
158
+ }
159
+ let mergesRange = "";
160
+ try {
161
+ mergesRange = (await git.raw(["rev-list", "--merges", `${resolvedHash}..HEAD`])).trim();
162
+ } catch {
163
+ }
164
+ if (mergesRange) {
165
+ sectionTitle("Unsafe automatic rewrite");
166
+ borderLine("Merge commits detected between target and HEAD.");
167
+ borderLine("Falling back to manual instructions (preserving previous behavior).");
168
+ borderLine();
169
+ sectionTitle("Apply manually");
170
+ borderLine(`1. git rebase -i ${resolvedHash}~1 --reword`);
171
+ borderLine("2. Mark the line as reword if needed.");
172
+ borderLine("3. Replace the message with:");
173
+ borderLine();
174
+ borderLine(candidate.title);
175
+ if (candidate.body) candidate.body.split("\n").forEach((l) => borderLine(l || void 0));
176
+ borderLine();
177
+ abortMessage();
178
+ return;
179
+ }
180
+ try {
181
+ const tree = (await git.raw(["show", "-s", "--format=%T", resolvedHash])).trim();
182
+ const parent = commit.parents[0];
183
+ const args = ["commit-tree", tree];
184
+ if (parent) args.push("-p", parent);
185
+ args.push("-m", full);
186
+ const newHash = (await git.raw(args)).trim();
187
+ const currentBranch = (await git.revparse(["--abbrev-ref", "HEAD"])).trim();
188
+ const rebaseTarget = currentBranch === "HEAD" ? "HEAD" : currentBranch;
189
+ await git.raw(["rebase", "--onto", newHash, resolvedHash, rebaseTarget]);
190
+ const afterBranch = (await git.revparse(["--abbrev-ref", "HEAD"])).trim();
191
+ if (afterBranch === "HEAD" && rebaseTarget !== "HEAD") {
192
+ try {
193
+ await git.checkout([rebaseTarget]);
194
+ } catch {
195
+ }
196
+ }
197
+ sectionTitle("Updated commit");
198
+ borderLine(`Rewrote ${resolvedHash.slice(0, 7)} \u2192 ${newHash.slice(0, 7)}`);
199
+ renderCommitBlock({ title: candidate.title, body: candidate.body, hideMessageLabel: true });
200
+ borderLine();
201
+ finalSuccess({ count: 1, startedAt });
202
+ } catch (e) {
203
+ borderLine("Automatic rewrite failed: " + (e?.message || e));
204
+ borderLine("If a rebase is in progress, resolve conflicts then run: git rebase --continue");
205
+ borderLine("Or abort with: git rebase --abort");
206
+ borderLine();
207
+ abortMessage();
208
+ }
209
+ }
210
+ export {
211
+ runReword
212
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kud/ai-conventional-commit-cli",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "type": "module",
5
5
  "description": "Opinionated, style-aware AI assistant for crafting and splitting git commits (opencode-based, provider-agnostic).",
6
6
  "bin": {
@@ -23,6 +23,7 @@
23
23
  },
24
24
  "dependencies": {
25
25
  "@commitlint/config-conventional": "^20.0.0",
26
+ "@inquirer/prompts": "8.0.1",
26
27
  "chalk": "^5.6.2",
27
28
  "clipanion": "^3.2.1",
28
29
  "cosmiconfig": "^9.0.0",