@kud/ai-conventional-commit-cli 1.0.1 → 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 +5 -1
- package/dist/chunk-F3BOAVBY.js +122 -0
- package/dist/config-RHGCFLHQ.js +12 -0
- package/dist/index.js +35 -14
- package/dist/reword-7GG233AE.js +212 -0
- package/package.json +1 -1
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
|
+
};
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
loadConfig
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-F3BOAVBY.js";
|
|
5
5
|
import {
|
|
6
6
|
OpenCodeProvider,
|
|
7
7
|
abortMessage,
|
|
@@ -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();
|
|
@@ -506,7 +506,7 @@ async function runSplit(config, desired) {
|
|
|
506
506
|
}
|
|
507
507
|
});
|
|
508
508
|
borderLine();
|
|
509
|
-
const ok = await select2({
|
|
509
|
+
const ok = config.yes || await select2({
|
|
510
510
|
message: "Use the commits?",
|
|
511
511
|
choices: [
|
|
512
512
|
{ name: "Yes", value: true },
|
|
@@ -656,7 +656,7 @@ async function runRefine(config, options) {
|
|
|
656
656
|
titleColor: (s) => chalk3.yellow(s)
|
|
657
657
|
});
|
|
658
658
|
borderLine();
|
|
659
|
-
const
|
|
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();
|
|
@@ -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-
|
|
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
|
`);
|
|
@@ -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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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 {
|
|
@@ -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