@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/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.1",
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: line.slice(3),
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.trim() || "(no raw output)", "Full error output");
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 p$1.multiselect({
3793
- message: "Select files to stage:",
3794
- options: sorted.map((f) => ({
3795
- label: `${statusLabel(f.status)} ${f.path}`,
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,