@kud/ai-conventional-commit-cli 3.0.2 → 3.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/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  loadConfig
4
- } from "./chunk-U7UVALKR.js";
4
+ } from "./chunk-6WHDHBGN.js";
5
5
  import {
6
6
  OpenCodeProvider,
7
7
  abortMessage,
@@ -16,7 +16,7 @@ import {
16
16
  formatCommitTitle,
17
17
  renderCommitBlock,
18
18
  sectionTitle
19
- } from "./chunk-KEEMHNNS.js";
19
+ } from "./chunk-YRVQGOVW.js";
20
20
 
21
21
  // src/index.ts
22
22
  import { Cli, Command, Option } from "clipanion";
@@ -29,10 +29,6 @@ import ora from "ora";
29
29
  import { simpleGit } from "simple-git";
30
30
  import crypto from "crypto";
31
31
  var git = simpleGit();
32
- var ensureStagedChanges = async () => {
33
- const status = await git.status();
34
- return status.staged.length > 0 || status.renamed.length > 0;
35
- };
36
32
  var getStagedDiffRaw = async () => {
37
33
  return git.diff(["--cached", "--unified=3", "--no-color", "-M"]);
38
34
  };
@@ -99,20 +95,20 @@ var parseDiffFromRaw = (raw) => {
99
95
  }
100
96
  return files;
101
97
  };
102
- var parseDiff = async () => {
103
- const raw = await getStagedDiffRaw();
98
+ var getStagedFilesAndDiff = async () => {
99
+ const [raw, status] = await Promise.all([getStagedDiffRaw(), git.status()]);
100
+ const hasStagedChanges = status.staged.length > 0 || status.renamed.length > 0;
104
101
  const parsed = parseDiffFromRaw(raw);
105
102
  if (parsed.length === 0) {
106
- const status = await git.status();
107
- const allStaged = [
108
- ...status.staged.map((f) => ({ file: f, hunks: [], additions: 0, deletions: 0 })),
109
- ...status.renamed.map((r) => ({ file: r.to, hunks: [], additions: 0, deletions: 0 }))
110
- ];
111
- if (allStaged.length > 0) {
112
- return allStaged;
113
- }
103
+ return {
104
+ files: [
105
+ ...status.staged.map((f) => ({ file: f, hunks: [], additions: 0, deletions: 0 })),
106
+ ...status.renamed.map((r) => ({ file: r.to, hunks: [], additions: 0, deletions: 0 }))
107
+ ],
108
+ hasStagedChanges
109
+ };
114
110
  }
115
- return parsed;
111
+ return { files: parsed, hasStagedChanges };
116
112
  };
117
113
  var getRecentCommitMessages = async (limit) => {
118
114
  const log = await git.log({ maxCount: limit });
@@ -132,10 +128,6 @@ var stageFiles = async (files) => {
132
128
  if (!files.length) return;
133
129
  await git.add(files);
134
130
  };
135
- var getStagedFiles = async () => {
136
- const status = await git.status();
137
- return [...status.staged, ...status.renamed.map((r) => r.to)];
138
- };
139
131
 
140
132
  // src/style.ts
141
133
  var buildStyleProfile = (messages) => {
@@ -216,11 +208,13 @@ import { join } from "path";
216
208
  import { select } from "@inquirer/prompts";
217
209
  async function runGenerate(config) {
218
210
  const startedAt = Date.now();
219
- if (!await ensureStagedChanges()) {
211
+ const provider = new OpenCodeProvider(config.model);
212
+ provider.warmup();
213
+ const { files, hasStagedChanges } = await getStagedFilesAndDiff();
214
+ if (!hasStagedChanges) {
220
215
  console.log("No staged changes.");
221
216
  return;
222
217
  }
223
- const files = await parseDiff();
224
218
  if (!files.length) {
225
219
  console.log("No diff content detected after staging. Aborting.");
226
220
  return;
@@ -273,18 +267,17 @@ async function runGenerate(config) {
273
267
  })();
274
268
  const phased = createPhasedSpinner(ora);
275
269
  const runStep = (label, fn) => phased.step(label, fn);
276
- let style;
277
- let plugins;
278
270
  let messages;
279
271
  let raw;
280
272
  let plan;
281
273
  let candidates = [];
282
- const provider = new OpenCodeProvider(config.model);
283
- style = await runStep("Profiling style", async () => {
284
- const history = await getRecentCommitMessages(config.styleSamples);
285
- return buildStyleProfile(history);
286
- });
287
- plugins = await runStep("Loading plugins", async () => loadPlugins(config));
274
+ const [style, plugins] = await runStep(
275
+ "Profiling style",
276
+ async () => Promise.all([
277
+ getRecentCommitMessages(config.styleSamples).then(buildStyleProfile),
278
+ loadPlugins(config)
279
+ ])
280
+ );
288
281
  messages = await runStep(
289
282
  "Building prompt",
290
283
  async () => buildGenerationMessages({ files, style, config, mode: "single" })
@@ -400,11 +393,13 @@ import { join as join2 } from "path";
400
393
  import { select as select2 } from "@inquirer/prompts";
401
394
  async function runSplit(config, desired) {
402
395
  const startedAt = Date.now();
403
- if (!await ensureStagedChanges()) {
396
+ const provider = new OpenCodeProvider(config.model);
397
+ provider.warmup();
398
+ const { files, hasStagedChanges } = await getStagedFilesAndDiff();
399
+ if (!hasStagedChanges) {
404
400
  console.log("No staged changes.");
405
401
  return;
406
402
  }
407
- const files = await parseDiff();
408
403
  if (!files.length) {
409
404
  console.log("No diff content detected after staging. Aborting.");
410
405
  return;
@@ -457,19 +452,18 @@ async function runSplit(config, desired) {
457
452
  })();
458
453
  const phased = createPhasedSpinner(ora2);
459
454
  const runStep = (label, fn) => phased.step(label, fn);
460
- await runStep("Clustering changes", async () => {
461
- clusterHunks(files);
462
- });
463
- const style = await runStep("Profiling style", async () => {
464
- const history = await getRecentCommitMessages(config.styleSamples);
465
- return buildStyleProfile(history);
466
- });
467
- const plugins = await runStep("Loading plugins", async () => loadPlugins(config));
455
+ clusterHunks(files);
456
+ const [style, plugins] = await runStep(
457
+ "Profiling style",
458
+ async () => Promise.all([
459
+ getRecentCommitMessages(config.styleSamples).then(buildStyleProfile),
460
+ loadPlugins(config)
461
+ ])
462
+ );
468
463
  const messages = await runStep(
469
464
  "Building prompt",
470
465
  async () => buildGenerationMessages({ files, style, config, mode: "split", desiredCommits: desired })
471
466
  );
472
- const provider = new OpenCodeProvider(config.model);
473
467
  const raw = await runStep(
474
468
  "Calling model",
475
469
  async () => provider.chat(messages, { maxTokens: config.maxTokens })
@@ -537,9 +531,8 @@ async function runSplit(config, desired) {
537
531
  let success = 0;
538
532
  for (const candidate of candidates) {
539
533
  await resetIndex();
540
- await stageFiles(candidate.files || []);
541
- const stagedNow = await getStagedFiles();
542
- if (!stagedNow.length) continue;
534
+ if (!candidate.files?.length) continue;
535
+ await stageFiles(candidate.files);
543
536
  try {
544
537
  await createCommit(candidate.title, candidate.body);
545
538
  success++;
@@ -873,7 +866,7 @@ var ModelsCommand = class extends Command {
873
866
  });
874
867
  async execute() {
875
868
  if (this.current) {
876
- const { loadConfigDetailed } = await import("./config-AZDENPAB.js");
869
+ const { loadConfigDetailed } = await import("./config-JEYG4CTJ.js");
877
870
  const { config } = await loadConfigDetailed();
878
871
  this.context.stdout.write(`${config.model} (source: ${config._sources.model})
879
872
  `);
@@ -913,7 +906,7 @@ var ModelsCommand = class extends Command {
913
906
  this.context.stdout.write(model + "\n");
914
907
  if (this.save) {
915
908
  try {
916
- const { saveGlobalConfig } = await import("./config-AZDENPAB.js");
909
+ const { saveGlobalConfig } = await import("./config-JEYG4CTJ.js");
917
910
  const path = saveGlobalConfig({ model });
918
911
  this.context.stdout.write(`Saved as default model in ${path}
919
912
  `);
@@ -948,7 +941,7 @@ var ConfigShowCommand = class extends Command {
948
941
  });
949
942
  json = Option.Boolean("--json", false, { description: "Output JSON including _sources" });
950
943
  async execute() {
951
- const { loadConfigDetailed } = await import("./config-AZDENPAB.js");
944
+ const { loadConfigDetailed } = await import("./config-JEYG4CTJ.js");
952
945
  const { config, raw } = await loadConfigDetailed();
953
946
  if (this.json) {
954
947
  this.context.stdout.write(JSON.stringify({ config, raw }, null, 2) + "\n");
@@ -973,7 +966,7 @@ var ConfigGetCommand = class extends Command {
973
966
  key = Option.String();
974
967
  withSource = Option.Boolean("--with-source", false, { description: "Append source label" });
975
968
  async execute() {
976
- const { loadConfigDetailed } = await import("./config-AZDENPAB.js");
969
+ const { loadConfigDetailed } = await import("./config-JEYG4CTJ.js");
977
970
  const { config } = await loadConfigDetailed();
978
971
  const key = this.key;
979
972
  if (!(key in config)) {
@@ -1030,7 +1023,7 @@ var ConfigSetCommand = class extends Command {
1030
1023
  } catch {
1031
1024
  }
1032
1025
  }
1033
- const { saveGlobalConfig } = await import("./config-AZDENPAB.js");
1026
+ const { saveGlobalConfig } = await import("./config-JEYG4CTJ.js");
1034
1027
  const path = saveGlobalConfig({ [this.key]: parsed });
1035
1028
  this.context.stdout.write(`Saved ${this.key} to ${path}
1036
1029
  `);
@@ -1054,7 +1047,7 @@ var RewordCommand = class extends Command {
1054
1047
  description: "Auto-confirm commit without prompting"
1055
1048
  });
1056
1049
  async execute() {
1057
- const { runReword } = await import("./reword-KIR2DMTO.js");
1050
+ const { runReword } = await import("./reword-2ASH5EH5.js");
1058
1051
  const config = await loadConfig();
1059
1052
  if (this.style) config.style = this.style;
1060
1053
  if (this.model) config.model = this.model;
@@ -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-YRVQGOVW.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
+ };
@@ -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-ZLYMV2NJ.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
+ };