@kyubiware/commit-mint 0.8.4 → 0.9.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 +19 -7
- package/dist/cli.mjs +144 -61
- package/dist/cli.mjs.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -107,17 +107,27 @@ each with its own message. The flow:
|
|
|
107
107
|
— the remaining groups are not committed.
|
|
108
108
|
|
|
109
109
|
`cmint` (no `-a`) shows a staging menu for any number of changed files
|
|
110
|
-
(including one). The menu is also the only place the `a`
|
|
111
|
-
|
|
110
|
+
(including one). The menu is also the only place the `a` and `c` hotkeys
|
|
111
|
+
toggle persistent modes, so it always runs:
|
|
112
112
|
|
|
113
113
|
```
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
114
|
+
Stage files for commit:
|
|
115
|
+
⚡ Auto-accept: OFF (press `a` to toggle)
|
|
116
|
+
🛡 Pre-commit checks: ON (press `c` to toggle)
|
|
117
|
+
|
|
118
|
+
● Stage all files
|
|
119
|
+
○ Auto-group into commits
|
|
120
|
+
○ Run checks
|
|
121
|
+
...
|
|
119
122
|
```
|
|
120
123
|
|
|
124
|
+
- **`a` — Auto-accept** — skip the message review step. Persisted in
|
|
125
|
+
`~/.commit-mint` as `auto-accept`.
|
|
126
|
+
- **`c` — Pre-commit checks** — when OFF, skip the user-defined pre-commit
|
|
127
|
+
checks (the `.cmintrc` phase). Persisted in `~/.commit-mint` as
|
|
128
|
+
`run-checks` (default `true`/ON). Same effect as `cmint -N`, but persists
|
|
129
|
+
across runs. Only shown when a `.cmintrc` file exists.
|
|
130
|
+
|
|
121
131
|
## Self-update (`cmint update`)
|
|
122
132
|
|
|
123
133
|
`cmint update` checks the npm registry for a newer version and, if one exists,
|
|
@@ -228,6 +238,8 @@ and **eslint**. Unrecognized output falls back to a single raw-stderr entry.
|
|
|
228
238
|
| `type` | — | Force commit type prefix |
|
|
229
239
|
| `timeout` | `10000` | AI request timeout in ms |
|
|
230
240
|
| `proxy` | — | Proxy URL for API requests |
|
|
241
|
+
| `auto-accept` | `false` | `a` hotkey: skip message review step |
|
|
242
|
+
| `run-checks` | `true` | `c` hotkey: run user-defined pre-commit checks |
|
|
231
243
|
| `--agent` | `false` | Headless JSON-output mode for AI agents |
|
|
232
244
|
|
|
233
245
|
API key lookup checks the env var first, then the INI file.
|
package/dist/cli.mjs
CHANGED
|
@@ -33,7 +33,7 @@ var __exportAll = (all, no_symbols) => {
|
|
|
33
33
|
//#region package.json
|
|
34
34
|
var package_default = {
|
|
35
35
|
name: "@kyubiware/commit-mint",
|
|
36
|
-
version: "0.
|
|
36
|
+
version: "0.9.0",
|
|
37
37
|
description: "🌿 AI-powered git commit tool — auto-group changed files, generate messages, run pre-commit checks",
|
|
38
38
|
type: "module",
|
|
39
39
|
bin: { "cmint": "./dist/cli.mjs" },
|
|
@@ -1662,6 +1662,42 @@ async function setAutoAccept(enabled) {
|
|
|
1662
1662
|
await writeConfig({ "auto-accept": value });
|
|
1663
1663
|
}
|
|
1664
1664
|
//#endregion
|
|
1665
|
+
//#region src/services/run-checks.ts
|
|
1666
|
+
/**
|
|
1667
|
+
* Parse a stored `run-checks` INI value into a boolean.
|
|
1668
|
+
*
|
|
1669
|
+
* Polarity is positive: `true` means "run user-defined pre-commit checks"
|
|
1670
|
+
* (the default behavior), `false` means "skip them".
|
|
1671
|
+
*
|
|
1672
|
+
* Accepts true variants ("true", "1", "yes" — case-insensitive) and boolean
|
|
1673
|
+
* values from ini.parse (which converts unquoted `true`/`false` to actual
|
|
1674
|
+
* booleans). Returns **true** for undefined / empty / unknown values so the
|
|
1675
|
+
* default behavior is to run checks — a fresh install with no INI key must
|
|
1676
|
+
* behave identically to `run-checks = true`.
|
|
1677
|
+
*/
|
|
1678
|
+
function parseRunChecksValue(value) {
|
|
1679
|
+
if (typeof value === "boolean") return value;
|
|
1680
|
+
if (typeof value !== "string" || !value) return true;
|
|
1681
|
+
return ![
|
|
1682
|
+
"false",
|
|
1683
|
+
"0",
|
|
1684
|
+
"no"
|
|
1685
|
+
].includes(value.toLowerCase());
|
|
1686
|
+
}
|
|
1687
|
+
/** Read the persisted run-checks preference from `~/.commit-mint`. Defaults to true. */
|
|
1688
|
+
async function getRunChecks() {
|
|
1689
|
+
const raw = (await readConfig())["run-checks"];
|
|
1690
|
+
const enabled = parseRunChecksValue(raw);
|
|
1691
|
+
debug("getRunChecks: raw=%s enabled=%s", raw, enabled);
|
|
1692
|
+
return enabled;
|
|
1693
|
+
}
|
|
1694
|
+
/** Persist the run-checks preference to `~/.commit-mint`. */
|
|
1695
|
+
async function setRunChecks(enabled) {
|
|
1696
|
+
const value = enabled ? "true" : "false";
|
|
1697
|
+
debug("setRunChecks: %s", value);
|
|
1698
|
+
await writeConfig({ "run-checks": value });
|
|
1699
|
+
}
|
|
1700
|
+
//#endregion
|
|
1665
1701
|
//#region src/ui/grouping.ts
|
|
1666
1702
|
async function showGroupingConfirmation(groups, excluded) {
|
|
1667
1703
|
debug("showGroupingConfirmation: %d groups, %d excluded", groups.length, excluded.length);
|
|
@@ -1728,7 +1764,9 @@ function showGroupedFiles(groups, changedFiles) {
|
|
|
1728
1764
|
/** Milliseconds to wait after stdin closes for quick exit failures. */
|
|
1729
1765
|
const GRACE_PERIOD_MS = 150;
|
|
1730
1766
|
async function copyToClipboard(content) {
|
|
1767
|
+
debug("clipboard: copying %d bytes", content.length);
|
|
1731
1768
|
for (const [cmd, args] of [
|
|
1769
|
+
["wl-copy", ["--foreground"]],
|
|
1732
1770
|
["wl-copy", []],
|
|
1733
1771
|
["xclip", ["-selection", "clipboard"]],
|
|
1734
1772
|
["xsel", ["--clipboard", "--input"]],
|
|
@@ -2258,7 +2296,7 @@ async function runAutoGroupFlow(changedFiles, flags) {
|
|
|
2258
2296
|
else outro(dim("Nothing to commit."));
|
|
2259
2297
|
return "committed";
|
|
2260
2298
|
}
|
|
2261
|
-
if (!flags.noCheck) {
|
|
2299
|
+
if (!flags.noCheck && await getRunChecks()) {
|
|
2262
2300
|
const { getRepoRoot } = await Promise.resolve().then(() => git_exports);
|
|
2263
2301
|
if (await runCheckPhaseInteractive(await getRepoRoot(), await resolveToRepoRoot(included.filter((f) => f.status !== "D").map((f) => f.path)), 6e4) === "cancelled") return "cancelled";
|
|
2264
2302
|
}
|
|
@@ -3793,13 +3831,13 @@ var ut = class extends m {
|
|
|
3793
3831
|
}
|
|
3794
3832
|
};
|
|
3795
3833
|
//#endregion
|
|
3796
|
-
//#region src/ui/
|
|
3834
|
+
//#region src/ui/toggle-select.ts
|
|
3797
3835
|
const ON_LABEL = styleText("green", "ON");
|
|
3798
3836
|
const OFF_LABEL = styleText("dim", "OFF");
|
|
3799
|
-
|
|
3800
|
-
|
|
3801
|
-
const
|
|
3802
|
-
return `${dim(
|
|
3837
|
+
function renderToggleState(t, state) {
|
|
3838
|
+
const label = state ? ON_LABEL : OFF_LABEL;
|
|
3839
|
+
const hint = dim(`(press \`${t.hotkey}\` to toggle)`);
|
|
3840
|
+
return `${dim(`${t.icon} ${t.label}:`)} ${label} ${hint}`;
|
|
3803
3841
|
}
|
|
3804
3842
|
/** Render a single select option line. */
|
|
3805
3843
|
function renderOption(opt, active) {
|
|
@@ -3812,68 +3850,91 @@ function renderOption(opt, active) {
|
|
|
3812
3850
|
return `${dim(S_RADIO_INACTIVE)} ${dim(text)}`;
|
|
3813
3851
|
}
|
|
3814
3852
|
/**
|
|
3815
|
-
* Select prompt with
|
|
3853
|
+
* Select prompt with inline hotkey toggles.
|
|
3816
3854
|
*
|
|
3817
|
-
* Renders a normal select list plus
|
|
3818
|
-
*
|
|
3819
|
-
*
|
|
3855
|
+
* Renders a normal select list plus one status line per toggle. Pressing a
|
|
3856
|
+
* toggle's hotkey flips its state in-place (the prompt re-renders) and fires
|
|
3857
|
+
* its `onToggle` callback so callers can persist the change.
|
|
3820
3858
|
*
|
|
3821
|
-
* Returns `{ value,
|
|
3822
|
-
*
|
|
3859
|
+
* Returns `{ value, toggles }` on submit, or the clack cancel symbol on
|
|
3860
|
+
* cancel. `toggles` is a map of `ToggleOption.key -> final boolean state`.
|
|
3823
3861
|
*/
|
|
3824
|
-
async function
|
|
3825
|
-
|
|
3862
|
+
async function selectWithToggles(opts) {
|
|
3863
|
+
const state = {};
|
|
3864
|
+
for (const t of opts.toggles) state[t.hotkey] = t.initial;
|
|
3826
3865
|
const prompt = new ut({
|
|
3827
3866
|
options: opts.options,
|
|
3828
3867
|
input: opts.input,
|
|
3829
3868
|
output: opts.output,
|
|
3830
|
-
render()
|
|
3831
|
-
const sym = symbol(this.state);
|
|
3832
|
-
const statusLine = renderStatus(autoAccept);
|
|
3833
|
-
const header = `${sym} ${opts.message}\n${dim(S_BAR)} ${statusLine}`;
|
|
3834
|
-
switch (this.state) {
|
|
3835
|
-
case "submit": {
|
|
3836
|
-
const selected = this.options[this.cursor];
|
|
3837
|
-
const text = selected.label ?? String(selected.value);
|
|
3838
|
-
return `${header}\n${dim(S_BAR)} ${dim(text)}`;
|
|
3839
|
-
}
|
|
3840
|
-
case "cancel": {
|
|
3841
|
-
const selected = this.options[this.cursor];
|
|
3842
|
-
const text = selected.label ?? String(selected.value);
|
|
3843
|
-
return `${header}\n${dim(S_BAR)} ${styleText(["strikethrough", "dim"], text)}\n${dim(S_BAR_END)}`;
|
|
3844
|
-
}
|
|
3845
|
-
default: return [
|
|
3846
|
-
header,
|
|
3847
|
-
...limitOptions({
|
|
3848
|
-
cursor: this.cursor,
|
|
3849
|
-
options: this.options,
|
|
3850
|
-
style: (opt, active) => renderOption(opt, active),
|
|
3851
|
-
maxItems: 7,
|
|
3852
|
-
output: opts.output ?? process.stdout
|
|
3853
|
-
}).map((line) => `${dim(S_BAR)} ${line}`),
|
|
3854
|
-
`${dim(S_BAR_END)} ${dim("↑/↓ navigate • Enter confirm • `a` toggle auto-accept")}`
|
|
3855
|
-
].join("\n");
|
|
3856
|
-
}
|
|
3857
|
-
}
|
|
3869
|
+
render: buildPromptRenderer(opts, state)
|
|
3858
3870
|
});
|
|
3859
3871
|
prompt.on("key", async (char) => {
|
|
3860
|
-
|
|
3861
|
-
|
|
3862
|
-
|
|
3863
|
-
|
|
3864
|
-
|
|
3865
|
-
|
|
3866
|
-
|
|
3867
|
-
|
|
3872
|
+
const toggle = opts.toggles.find((t) => t.hotkey === char);
|
|
3873
|
+
if (!toggle) return;
|
|
3874
|
+
state[toggle.hotkey] = !state[toggle.hotkey];
|
|
3875
|
+
debug("%s toggled to %s", toggle.label, state[toggle.hotkey]);
|
|
3876
|
+
try {
|
|
3877
|
+
await toggle.onToggle?.(state[toggle.hotkey]);
|
|
3878
|
+
} catch (err) {
|
|
3879
|
+
debug("onToggle threw (ignored): %s", err instanceof Error ? err.message : String(err));
|
|
3868
3880
|
}
|
|
3869
3881
|
});
|
|
3870
3882
|
const result = await prompt.prompt();
|
|
3871
3883
|
if (q(result)) return result;
|
|
3872
3884
|
return {
|
|
3873
3885
|
value: result,
|
|
3874
|
-
|
|
3886
|
+
toggles: buildTogglesMap(opts, state)
|
|
3887
|
+
};
|
|
3888
|
+
}
|
|
3889
|
+
/**
|
|
3890
|
+
* Build a render callback for the SelectPrompt.
|
|
3891
|
+
*
|
|
3892
|
+
* Closed over `opts` (the toggle-select options) and `state` (the mutable
|
|
3893
|
+
* hotkey → boolean map) so the inline render function can access toggle
|
|
3894
|
+
* state without encoding it in `this`.
|
|
3895
|
+
*/
|
|
3896
|
+
function buildPromptRenderer(opts, state) {
|
|
3897
|
+
const optionList = opts.options;
|
|
3898
|
+
const toggleList = opts.toggles;
|
|
3899
|
+
return function() {
|
|
3900
|
+
const sym = symbol(this.state);
|
|
3901
|
+
const statusLines = toggleList.map((t) => renderToggleState(t, state[t.hotkey])).join("\n");
|
|
3902
|
+
const header = `${sym} ${opts.message}\n${dim(S_BAR)} ${statusLines}`;
|
|
3903
|
+
switch (this.state) {
|
|
3904
|
+
case "submit": {
|
|
3905
|
+
const selected = optionList[this.cursor];
|
|
3906
|
+
const text = selected.label ?? String(selected.value);
|
|
3907
|
+
return `${header}\n${dim(S_BAR)} ${dim(text)}`;
|
|
3908
|
+
}
|
|
3909
|
+
case "cancel": {
|
|
3910
|
+
const selected = optionList[this.cursor];
|
|
3911
|
+
const text = selected.label ?? String(selected.value);
|
|
3912
|
+
return `${header}\n${dim(S_BAR)} ${styleText(["strikethrough", "dim"], text)}\n${dim(S_BAR_END)}`;
|
|
3913
|
+
}
|
|
3914
|
+
default: {
|
|
3915
|
+
const lines = limitOptions({
|
|
3916
|
+
cursor: this.cursor,
|
|
3917
|
+
options: optionList,
|
|
3918
|
+
style: (opt, active) => renderOption(opt, active),
|
|
3919
|
+
maxItems: 7,
|
|
3920
|
+
output: opts.output ?? process.stdout
|
|
3921
|
+
}).map((line) => `${dim(S_BAR)} ${line}`);
|
|
3922
|
+
const hotkeysHint = toggleList.map((t) => `\`${t.hotkey}\` toggle ${t.label.toLowerCase()}`).join(" • ");
|
|
3923
|
+
return [
|
|
3924
|
+
header,
|
|
3925
|
+
...lines,
|
|
3926
|
+
`${dim(S_BAR_END)} ${dim(`↑/↓ navigate • Enter confirm • ${hotkeysHint}`)}`
|
|
3927
|
+
].join("\n");
|
|
3928
|
+
}
|
|
3929
|
+
}
|
|
3875
3930
|
};
|
|
3876
3931
|
}
|
|
3932
|
+
/** Collate the final toggle states keyed by `ToggleOption.key`. */
|
|
3933
|
+
function buildTogglesMap(opts, state) {
|
|
3934
|
+
const togglesByKey = {};
|
|
3935
|
+
for (const t of opts.toggles) togglesByKey[t.key] = state[t.hotkey];
|
|
3936
|
+
return togglesByKey;
|
|
3937
|
+
}
|
|
3877
3938
|
//#endregion
|
|
3878
3939
|
//#region src/ui/staging-menu.ts
|
|
3879
3940
|
async function showStagingMenu(files, hasChecks) {
|
|
@@ -3903,12 +3964,25 @@ async function showStagingMenu(files, hasChecks) {
|
|
|
3903
3964
|
p$1.note(lines.join("\n"), `${files.length} file${files.length !== 1 ? "s" : ""}`);
|
|
3904
3965
|
const initialAutoAccept = await getAutoAccept();
|
|
3905
3966
|
debug("showStagingMenu: initial auto-accept=%s", initialAutoAccept);
|
|
3906
|
-
const
|
|
3967
|
+
const initialRunChecks = hasChecks ? await getRunChecks() : true;
|
|
3968
|
+
debug("showStagingMenu: initial run-checks=%s", initialRunChecks);
|
|
3969
|
+
const selectResult = await selectWithToggles({
|
|
3907
3970
|
message: "Stage files for commit:",
|
|
3908
|
-
|
|
3909
|
-
|
|
3910
|
-
|
|
3911
|
-
|
|
3971
|
+
toggles: [{
|
|
3972
|
+
key: "autoAccept",
|
|
3973
|
+
hotkey: "a",
|
|
3974
|
+
label: "Auto-accept",
|
|
3975
|
+
icon: "⚡",
|
|
3976
|
+
initial: initialAutoAccept,
|
|
3977
|
+
onToggle: (next) => setAutoAccept(next)
|
|
3978
|
+
}, ...hasChecks ? [{
|
|
3979
|
+
key: "runChecks",
|
|
3980
|
+
hotkey: "c",
|
|
3981
|
+
label: "Pre-commit checks",
|
|
3982
|
+
icon: "🛡",
|
|
3983
|
+
initial: initialRunChecks,
|
|
3984
|
+
onToggle: (next) => setRunChecks(next)
|
|
3985
|
+
}] : []],
|
|
3912
3986
|
options: [
|
|
3913
3987
|
...files.length > 1 ? [{
|
|
3914
3988
|
label: "Auto-group into commits",
|
|
@@ -3945,7 +4019,7 @@ async function showStagingMenu(files, hasChecks) {
|
|
|
3945
4019
|
return null;
|
|
3946
4020
|
}
|
|
3947
4021
|
const choice = selectResult.value;
|
|
3948
|
-
debug("showStagingMenu: choice=%s autoAccept=%s", choice, selectResult.autoAccept);
|
|
4022
|
+
debug("showStagingMenu: choice=%s autoAccept=%s runChecks=%s", choice, selectResult.toggles.autoAccept, selectResult.toggles.runChecks);
|
|
3949
4023
|
if (p$1.isCancel(choice) || choice === "cancel") return null;
|
|
3950
4024
|
if (choice === "autogroup") return "autogroup";
|
|
3951
4025
|
if (choice === "checks") return "checks";
|
|
@@ -4073,7 +4147,10 @@ async function commitCommand(flags, version) {
|
|
|
4073
4147
|
debug("Changed files:", changedFiles.length);
|
|
4074
4148
|
const s = spinner();
|
|
4075
4149
|
try {
|
|
4076
|
-
if (flags.
|
|
4150
|
+
if (flags.single) {
|
|
4151
|
+
debug("Single-commit mode: staging all files");
|
|
4152
|
+
await stageAll();
|
|
4153
|
+
} else if (flags.auto) {
|
|
4077
4154
|
if (flags.message) {
|
|
4078
4155
|
outro(red("--message flag is not compatible with auto-group mode."));
|
|
4079
4156
|
return;
|
|
@@ -4093,7 +4170,7 @@ async function commitCommand(flags, version) {
|
|
|
4093
4170
|
process.exit(1);
|
|
4094
4171
|
}
|
|
4095
4172
|
changedFiles = await getChangedFiles();
|
|
4096
|
-
await runPreCommitChecks(changedFiles,
|
|
4173
|
+
if (!flags.noCheck && await getRunChecks()) await runPreCommitChecks(changedFiles, false);
|
|
4097
4174
|
const diffResult = await getStagedDiff();
|
|
4098
4175
|
if (!diffResult) {
|
|
4099
4176
|
debug("No staged changes found after staging");
|
|
@@ -4157,7 +4234,7 @@ async function commitCommand(flags, version) {
|
|
|
4157
4234
|
}
|
|
4158
4235
|
s.stop("Message generated");
|
|
4159
4236
|
}
|
|
4160
|
-
if (await getAutoAccept()) {
|
|
4237
|
+
if (flags.single || await getAutoAccept()) {
|
|
4161
4238
|
debug("Auto-accept ON: skipping review step");
|
|
4162
4239
|
log.info(message);
|
|
4163
4240
|
} else {
|
|
@@ -4629,6 +4706,12 @@ cli({
|
|
|
4629
4706
|
type: Boolean,
|
|
4630
4707
|
description: "AI agent mode: non-interactive auto-group with JSON output",
|
|
4631
4708
|
default: false
|
|
4709
|
+
},
|
|
4710
|
+
single: {
|
|
4711
|
+
type: Boolean,
|
|
4712
|
+
description: "Stage all files as a single commit with AI message (non-interactive)",
|
|
4713
|
+
alias: "s",
|
|
4714
|
+
default: false
|
|
4632
4715
|
}
|
|
4633
4716
|
},
|
|
4634
4717
|
commands: [
|