@kyubiware/commit-mint 0.9.1 → 0.9.3
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 +277 -224
- package/dist/bin.mjs +156 -13
- package/dist/bin.mjs.map +1 -1
- package/package.json +1 -1
package/dist/bin.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { cli, command } from "cleye";
|
|
3
3
|
import * as p$1 from "@clack/prompts";
|
|
4
|
-
import { S_BAR, S_BAR_END, S_RADIO_ACTIVE, S_RADIO_INACTIVE, intro, isCancel, limitOptions, log, outro, spinner, symbol } from "@clack/prompts";
|
|
4
|
+
import { S_BAR, S_BAR_END, S_CHECKBOX_ACTIVE, S_CHECKBOX_INACTIVE, S_CHECKBOX_SELECTED, S_RADIO_ACTIVE, S_RADIO_INACTIVE, intro, isCancel, limitOptions, log, outro, spinner, symbol } from "@clack/prompts";
|
|
5
5
|
import { bold, cyan, dim, green, red, yellow } from "kolorist";
|
|
6
6
|
import { appendFileSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
7
7
|
import os from "node:os";
|
|
@@ -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.9.
|
|
36
|
+
version: "0.9.3",
|
|
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/bin.mjs" },
|
|
@@ -681,9 +681,15 @@ async function getChangedFiles() {
|
|
|
681
681
|
if (!stdout.trim()) return [];
|
|
682
682
|
const files = stdout.split("\n").filter(Boolean).map((line) => {
|
|
683
683
|
const indexStatus = line[0];
|
|
684
|
+
const worktreeStatus = line[1];
|
|
685
|
+
let path = line.slice(3);
|
|
686
|
+
if (indexStatus === "R" || worktreeStatus === "R") {
|
|
687
|
+
const arrow = path.lastIndexOf(" -> ");
|
|
688
|
+
if (arrow !== -1) path = path.slice(arrow + 4);
|
|
689
|
+
}
|
|
684
690
|
return {
|
|
685
691
|
status: line.slice(0, 2).trim(),
|
|
686
|
-
path
|
|
692
|
+
path,
|
|
687
693
|
staged: indexStatus !== " " && indexStatus !== "?"
|
|
688
694
|
};
|
|
689
695
|
});
|
|
@@ -2462,6 +2468,21 @@ function truncate(message, maxLength) {
|
|
|
2462
2468
|
if (collapsed.length <= maxLength) return collapsed;
|
|
2463
2469
|
return `${collapsed.slice(0, Math.max(0, maxLength - 1))}…`;
|
|
2464
2470
|
}
|
|
2471
|
+
/**
|
|
2472
|
+
* Filter noisy lines from vitest/jest test output.
|
|
2473
|
+
* Strips stack trace frames (❯ prefix) and horizontal rule separators (⎯⎯⎯ lines),
|
|
2474
|
+
* while preserving FAIL lines, assertion messages, diff content, and the final summary.
|
|
2475
|
+
*/
|
|
2476
|
+
function filterTestOutput(output) {
|
|
2477
|
+
return output.split("\n").filter((line) => {
|
|
2478
|
+
if (/^\s*[❯>]\s/.test(line)) return false;
|
|
2479
|
+
const stripped = line.replace(/\s/g, "");
|
|
2480
|
+
if (stripped.length > 10) {
|
|
2481
|
+
if ((stripped.match(/[⎯\-═]/g) ?? []).length / stripped.length > .5) return false;
|
|
2482
|
+
}
|
|
2483
|
+
return true;
|
|
2484
|
+
}).join("\n").replace(/\n{3,}/g, "\n\n").trim();
|
|
2485
|
+
}
|
|
2465
2486
|
async function showCheckFailureMenu(errors, rawStderr, onRetry) {
|
|
2466
2487
|
debug("showCheckFailureMenu: %d errors", errors.length);
|
|
2467
2488
|
let clipboardCopied = false;
|
|
@@ -2501,13 +2522,13 @@ async function showCheckFailureMenu(errors, rawStderr, onRetry) {
|
|
|
2501
2522
|
debug("showCheckFailureMenu: user chose %s", choice);
|
|
2502
2523
|
switch (choice) {
|
|
2503
2524
|
case "copy":
|
|
2504
|
-
if (await copyToClipboard(rawStderr)) {
|
|
2525
|
+
if (await copyToClipboard(filterTestOutput(rawStderr))) {
|
|
2505
2526
|
clipboardCopied = true;
|
|
2506
2527
|
p$1.log.step(green("Copied to clipboard."));
|
|
2507
2528
|
} else p$1.log.warn(red("No clipboard tool found. Install xclip, wl-copy, or xsel."));
|
|
2508
2529
|
continue;
|
|
2509
2530
|
case "view":
|
|
2510
|
-
p$1.note(rawStderr
|
|
2531
|
+
p$1.note(filterTestOutput(rawStderr) || "(no raw output)", "Full error output");
|
|
2511
2532
|
continue;
|
|
2512
2533
|
case "retry":
|
|
2513
2534
|
if (onRetry) return "retried";
|
|
@@ -3564,6 +3585,52 @@ let m = class {
|
|
|
3564
3585
|
}
|
|
3565
3586
|
}
|
|
3566
3587
|
};
|
|
3588
|
+
let nt = class extends m {
|
|
3589
|
+
options;
|
|
3590
|
+
cursor = 0;
|
|
3591
|
+
get _value() {
|
|
3592
|
+
return this.options[this.cursor].value;
|
|
3593
|
+
}
|
|
3594
|
+
get _enabledOptions() {
|
|
3595
|
+
return this.options.filter((t) => t.disabled !== !0);
|
|
3596
|
+
}
|
|
3597
|
+
toggleAll() {
|
|
3598
|
+
const t = this._enabledOptions, s = this.value !== void 0 && this.value.length === t.length;
|
|
3599
|
+
this.value = s ? [] : t.map((e) => e.value);
|
|
3600
|
+
}
|
|
3601
|
+
toggleInvert() {
|
|
3602
|
+
const t = this.value;
|
|
3603
|
+
if (!t) return;
|
|
3604
|
+
const s = this._enabledOptions.filter((e) => !t.includes(e.value));
|
|
3605
|
+
this.value = s.map((e) => e.value);
|
|
3606
|
+
}
|
|
3607
|
+
toggleValue() {
|
|
3608
|
+
this.value === void 0 && (this.value = []);
|
|
3609
|
+
const t = this.value.includes(this._value);
|
|
3610
|
+
this.value = t ? this.value.filter((s) => s !== this._value) : [...this.value, this._value];
|
|
3611
|
+
}
|
|
3612
|
+
constructor(t) {
|
|
3613
|
+
super(t, !1), this.options = t.options, this.value = [...t.initialValues ?? []];
|
|
3614
|
+
const s = Math.max(this.options.findIndex(({ value: e }) => e === t.cursorAt), 0);
|
|
3615
|
+
this.cursor = this.options[s].disabled ? f(s, 1, this.options) : s, this.on("key", (e) => {
|
|
3616
|
+
e === "a" && this.toggleAll(), e === "i" && this.toggleInvert();
|
|
3617
|
+
}), this.on("cursor", (e) => {
|
|
3618
|
+
switch (e) {
|
|
3619
|
+
case "left":
|
|
3620
|
+
case "up":
|
|
3621
|
+
this.cursor = f(this.cursor, -1, this.options);
|
|
3622
|
+
break;
|
|
3623
|
+
case "down":
|
|
3624
|
+
case "right":
|
|
3625
|
+
this.cursor = f(this.cursor, 1, this.options);
|
|
3626
|
+
break;
|
|
3627
|
+
case "space":
|
|
3628
|
+
this.toggleValue();
|
|
3629
|
+
break;
|
|
3630
|
+
}
|
|
3631
|
+
});
|
|
3632
|
+
}
|
|
3633
|
+
};
|
|
3567
3634
|
var ut = class extends m {
|
|
3568
3635
|
options;
|
|
3569
3636
|
cursor = 0;
|
|
@@ -3592,6 +3659,86 @@ var ut = class extends m {
|
|
|
3592
3659
|
}
|
|
3593
3660
|
};
|
|
3594
3661
|
//#endregion
|
|
3662
|
+
//#region src/ui/file-multiselect.ts
|
|
3663
|
+
/**
|
|
3664
|
+
* Render a checkbox-style option line with 4 visual states:
|
|
3665
|
+
* active-selected, selected, active, inactive
|
|
3666
|
+
*/
|
|
3667
|
+
function renderCheckboxOption(label, selected, active) {
|
|
3668
|
+
if (selected && active) return `${green(S_CHECKBOX_SELECTED)} ${label}`;
|
|
3669
|
+
if (selected) return `${dim(S_CHECKBOX_SELECTED)} ${dim(label)}`;
|
|
3670
|
+
if (active) return `${green(S_CHECKBOX_ACTIVE)} ${label}`;
|
|
3671
|
+
return `${dim(S_CHECKBOX_INACTIVE)} ${dim(label)}`;
|
|
3672
|
+
}
|
|
3673
|
+
/**
|
|
3674
|
+
* Build the render function for a MultiSelectPrompt with file-multiselect
|
|
3675
|
+
* styling and a hotkey hint line at the bottom.
|
|
3676
|
+
*/
|
|
3677
|
+
function buildMultiSelectRender(message, options, output) {
|
|
3678
|
+
return function() {
|
|
3679
|
+
const header = `${symbol(this.state)} ${message}`;
|
|
3680
|
+
const value = this.value ?? [];
|
|
3681
|
+
const cursor = this.cursor;
|
|
3682
|
+
switch (this.state) {
|
|
3683
|
+
case "submit": {
|
|
3684
|
+
const labels = value.map((v) => {
|
|
3685
|
+
return options.find((o) => o.value === v)?.label ?? String(v);
|
|
3686
|
+
}).join(dim(", "));
|
|
3687
|
+
return `${header}\n${dim(S_BAR)} ${dim(labels)}`;
|
|
3688
|
+
}
|
|
3689
|
+
case "cancel": {
|
|
3690
|
+
const cancelled = styleText(["strikethrough", "dim"], "Cancelled");
|
|
3691
|
+
return `${header}\n${dim(S_BAR_END)} ${cancelled}`;
|
|
3692
|
+
}
|
|
3693
|
+
default: {
|
|
3694
|
+
const lines = limitOptions({
|
|
3695
|
+
cursor,
|
|
3696
|
+
options,
|
|
3697
|
+
style: (opt, active) => renderCheckboxOption(opt.label, value.includes(opt.value), active),
|
|
3698
|
+
maxItems: 7,
|
|
3699
|
+
output: output ?? process.stdout
|
|
3700
|
+
}).map((line) => `${dim(S_BAR)} ${line}`);
|
|
3701
|
+
const hintLine = "↑/↓ navigate · space select · enter confirm · A select all · N select none";
|
|
3702
|
+
return [
|
|
3703
|
+
header,
|
|
3704
|
+
...lines,
|
|
3705
|
+
`${dim(S_BAR_END)} ${dim(hintLine)}`
|
|
3706
|
+
].join("\n");
|
|
3707
|
+
}
|
|
3708
|
+
}
|
|
3709
|
+
};
|
|
3710
|
+
}
|
|
3711
|
+
/**
|
|
3712
|
+
* Multi-select prompt with extended hotkeys for file selection.
|
|
3713
|
+
*
|
|
3714
|
+
* Built-in (from `@clack/core`'s `MultiSelectPrompt`):
|
|
3715
|
+
* `a` — toggle all (select all / deselect all)
|
|
3716
|
+
* `i` — invert selection
|
|
3717
|
+
*
|
|
3718
|
+
* Extended hotkeys:
|
|
3719
|
+
* `A` (shift+A) — select all unconditionally
|
|
3720
|
+
* `N` — select none (deselect all)
|
|
3721
|
+
*/
|
|
3722
|
+
async function fileMultiSelect(message, options, opts) {
|
|
3723
|
+
const required = opts?.required ?? true;
|
|
3724
|
+
const prompt = new nt({
|
|
3725
|
+
options,
|
|
3726
|
+
required,
|
|
3727
|
+
input: opts?.input,
|
|
3728
|
+
output: opts?.output,
|
|
3729
|
+
validate: (values) => {
|
|
3730
|
+
if (required && (!values || values.length === 0)) return "Please select at least one option.";
|
|
3731
|
+
},
|
|
3732
|
+
render: buildMultiSelectRender(message, options, opts?.output)
|
|
3733
|
+
});
|
|
3734
|
+
prompt.on("key", (char, key) => {
|
|
3735
|
+
if (!char) return;
|
|
3736
|
+
if (char === "a" && key?.shift) prompt.value = options.map((o) => o.value);
|
|
3737
|
+
if (char === "n") prompt.value = [];
|
|
3738
|
+
});
|
|
3739
|
+
return await prompt.prompt();
|
|
3740
|
+
}
|
|
3741
|
+
//#endregion
|
|
3595
3742
|
//#region src/ui/toggle-select.ts
|
|
3596
3743
|
const ON_LABEL = styleText("green", "ON");
|
|
3597
3744
|
const OFF_LABEL = styleText("dim", "OFF");
|
|
@@ -3789,14 +3936,10 @@ async function showStagingMenu(files, hasChecks) {
|
|
|
3789
3936
|
files: files.map((f) => f.path),
|
|
3790
3937
|
all: true
|
|
3791
3938
|
};
|
|
3792
|
-
const selected = await
|
|
3793
|
-
|
|
3794
|
-
|
|
3795
|
-
|
|
3796
|
-
value: f.path
|
|
3797
|
-
})),
|
|
3798
|
-
required: true
|
|
3799
|
-
});
|
|
3939
|
+
const selected = await fileMultiSelect("Select files to stage:", sorted.map((f) => ({
|
|
3940
|
+
label: `${statusLabel(f.status)} ${f.path}`,
|
|
3941
|
+
value: f.path
|
|
3942
|
+
})), { required: true });
|
|
3800
3943
|
if (p$1.isCancel(selected)) return null;
|
|
3801
3944
|
return {
|
|
3802
3945
|
files: selected,
|