@kyubiware/commit-mint 0.9.2 โ†’ 0.9.4

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 CHANGED
@@ -274,7 +274,7 @@ returns either.
274
274
  | Form | Behavior |
275
275
  |---|---|
276
276
  | `string` | Matched files are appended as trailing arguments. Paths with spaces are quoted automatically. |
277
- | `string[]` | Commands run sequentially, each as a separate command. First failure stops the run. |
277
+ | `string[]` | Commands run sequentially, each as a separate command. All commands run regardless of failures. |
278
278
  | `(files) => string \| string[]` | Function receives the matched files. Use when the command depends on the file list. |
279
279
 
280
280
  **String command:**
@@ -313,8 +313,8 @@ export default {
313
313
 
314
314
  - Checks run after `git add`, before the AI call.
315
315
  - Globs are processed in declaration order.
316
- - Commands run sequentially per glob. First failure stops the run (fail-fast)
317
- and skips remaining globs.
316
+ - Commands run sequentially per glob. All commands and all globs always run;
317
+ failures are collected and returned together.
318
318
  - 60s timeout per command. ENOENT (command not found) and timeouts are
319
319
  reported back to the menu as their own error.
320
320
  - Skipped entirely with `cmint -N` or the `c` hotkey toggle.
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.2",
36
+ version: "0.9.4",
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" },
@@ -2287,30 +2287,29 @@ function resolveCommands(commands, matchedFiles) {
2287
2287
  /**
2288
2288
  * Run resolved commands for a single glob entry, appending results.
2289
2289
  * Function-originated commands run as-is; string commands get matched files appended.
2290
- * Returns false if any command fails (for fail-fast signaling).
2290
+ * All commands run regardless of individual failures.
2291
2291
  */
2292
- async function runCommandsForGlob(cmds, matchedFiles, timeout, results, repoRoot) {
2292
+ async function runCommandsForGlob(cmds, matchedFiles, timeout, results, repoRoot, observer) {
2293
2293
  for (const { command, fromFunction } of cmds) {
2294
2294
  const fullCommand = fromFunction ? command : buildCommand(command, matchedFiles);
2295
+ const tool = extractToolName(fullCommand) ?? fullCommand.split(" ")[0];
2295
2296
  debug("runCommandsForGlob: running '%s'", fullCommand);
2297
+ observer?.onStart?.(tool, fullCommand, matchedFiles);
2296
2298
  const result = await runCommand(fullCommand, timeout, repoRoot);
2297
2299
  results.push({
2298
2300
  ...result,
2299
2301
  files: matchedFiles
2300
2302
  });
2301
- if (!result.ok) {
2302
- debug("runCommandsForGlob: check failed, stopping (fail-fast)");
2303
- return false;
2304
- }
2303
+ observer?.onResult?.(result);
2304
+ if (!result.ok) debug("runCommandsForGlob: check failed, continuing to next command");
2305
2305
  }
2306
- return true;
2307
2306
  }
2308
2307
  /**
2309
2308
  * Run all user-defined checks from .cmintrc against staged files.
2310
2309
  * Returns a no-op result when no config exists.
2311
- * Fail-fast: stops on first error.
2310
+ * All checks always run; failures are collected and returned together.
2312
2311
  */
2313
- async function runAllChecks(repoRoot, stagedFiles, timeout) {
2312
+ async function runAllChecks(repoRoot, stagedFiles, timeout, observer) {
2314
2313
  debug("runAllChecks: %d staged files, checking for config in %s", stagedFiles.length, repoRoot);
2315
2314
  if (!await detectConfig(repoRoot)) {
2316
2315
  debug("runAllChecks: no config found, skipping checks");
@@ -2329,10 +2328,7 @@ async function runAllChecks(repoRoot, stagedFiles, timeout) {
2329
2328
  continue;
2330
2329
  }
2331
2330
  debug("runAllChecks: pattern '%s' matched %d files", glob, matchedFiles.length);
2332
- if (!await runCommandsForGlob(resolveCommands(commands, matchedFiles), matchedFiles, timeout, results, repoRoot)) return {
2333
- ok: false,
2334
- results
2335
- };
2331
+ await runCommandsForGlob(resolveCommands(commands, matchedFiles), matchedFiles, timeout, results, repoRoot, observer);
2336
2332
  }
2337
2333
  const ok = results.every((r) => r.ok);
2338
2334
  debug("runAllChecks: complete โ€” ok=%s, %d results", ok, results.length);
@@ -2414,7 +2410,9 @@ function extractEslintDiagnostics(raw) {
2414
2410
  function extractTestFailures(raw) {
2415
2411
  const failures = [];
2416
2412
  const seen = /* @__PURE__ */ new Set();
2417
- for (const line of raw.split("\n")) {
2413
+ const lines = raw.split("\n");
2414
+ for (let i = 0; i < lines.length; i++) {
2415
+ const line = lines[i];
2418
2416
  const match = TEST_FILE_FAIL.exec(line);
2419
2417
  if (!match) continue;
2420
2418
  const file = (match[1] ?? "").trim();
@@ -2423,13 +2421,35 @@ function extractTestFailures(raw) {
2423
2421
  const key = `${file}\u0000${name}`;
2424
2422
  if (seen.has(key)) continue;
2425
2423
  seen.add(key);
2424
+ const message = scanForwardForMessage(lines, i + 1, TEST_FILE_FAIL);
2426
2425
  failures.push({
2427
2426
  file,
2428
- name
2427
+ name,
2428
+ message
2429
2429
  });
2430
2430
  }
2431
2431
  return failures;
2432
2432
  }
2433
+ /**
2434
+ * Scan forward from `startIndex` to find the first assertion error message line
2435
+ * after a FAIL header. Skips blank lines, separator dashes, stack frames,
2436
+ * subsequent FAIL lines, and vitest summary footers.
2437
+ */
2438
+ function scanForwardForMessage(lines, startIndex, testFileFailRegex) {
2439
+ for (let j = startIndex; j < lines.length; j++) {
2440
+ const rawLine = lines[j];
2441
+ const trimmed = rawLine.trim();
2442
+ if (!trimmed) continue;
2443
+ const stripped = trimmed.replace(/\s/g, "");
2444
+ if (stripped.length > 5) {
2445
+ if ((stripped.match(/[โŽฏ\-โ•]/g) ?? []).length / stripped.length > .5) continue;
2446
+ }
2447
+ if (/^\s*[โฏ>]\s/.test(rawLine)) continue;
2448
+ if (testFileFailRegex.test(trimmed)) break;
2449
+ if (/^(Test Files|Tests)\s/.test(trimmed)) break;
2450
+ return truncate(trimmed, MAX_SUMMARY_LINE_LENGTH);
2451
+ }
2452
+ }
2433
2453
  function formatTestFailureSummary(failures, tool) {
2434
2454
  const total = failures.length;
2435
2455
  const visible = failures.slice(0, MAX_TEST_FAILURES);
@@ -2440,13 +2460,19 @@ function formatTestFailureSummary(failures, tool) {
2440
2460
  const lines = [` ${red("โ€ข")} [${tool}] ${total} failed ${testNoun} in ${fileCount} ${fileNoun}`];
2441
2461
  const byFile = /* @__PURE__ */ new Map();
2442
2462
  for (const failure of visible) {
2443
- const names = byFile.get(failure.file) ?? [];
2444
- names.push(failure.name);
2445
- byFile.set(failure.file, names);
2463
+ const entries = byFile.get(failure.file) ?? [];
2464
+ entries.push({
2465
+ name: failure.name,
2466
+ message: failure.message
2467
+ });
2468
+ byFile.set(failure.file, entries);
2446
2469
  }
2447
- for (const [file, names] of byFile) {
2470
+ for (const [file, entries] of byFile) {
2448
2471
  lines.push(` ${truncate(file, MAX_SUMMARY_LINE_LENGTH)}`);
2449
- for (const name of names) lines.push(` ${red("ร—")} ${truncate(name, MAX_SUMMARY_LINE_LENGTH)}`);
2472
+ for (const entry of entries) {
2473
+ lines.push(` ${red("ร—")} ${truncate(entry.name, MAX_SUMMARY_LINE_LENGTH)}`);
2474
+ if (entry.message) lines.push(` ${dim("โ†’")} ${entry.message}`);
2475
+ }
2450
2476
  }
2451
2477
  if (hidden > 0) lines.push(dim(` +${hidden} more failed ${hidden === 1 ? "test" : "tests"}. View full output for details.`));
2452
2478
  return lines.join("\n");
@@ -2468,6 +2494,21 @@ function truncate(message, maxLength) {
2468
2494
  if (collapsed.length <= maxLength) return collapsed;
2469
2495
  return `${collapsed.slice(0, Math.max(0, maxLength - 1))}โ€ฆ`;
2470
2496
  }
2497
+ /**
2498
+ * Filter noisy lines from vitest/jest test output.
2499
+ * Strips stack trace frames (โฏ prefix) and horizontal rule separators (โŽฏโŽฏโŽฏ lines),
2500
+ * while preserving FAIL lines, assertion messages, diff content, and the final summary.
2501
+ */
2502
+ function filterTestOutput(output) {
2503
+ return output.split("\n").filter((line) => {
2504
+ if (/^\s*[โฏ>]\s/.test(line)) return false;
2505
+ const stripped = line.replace(/\s/g, "");
2506
+ if (stripped.length > 10) {
2507
+ if ((stripped.match(/[โŽฏ\-โ•]/g) ?? []).length / stripped.length > .5) return false;
2508
+ }
2509
+ return true;
2510
+ }).join("\n").replace(/\n{3,}/g, "\n\n").trim();
2511
+ }
2471
2512
  async function showCheckFailureMenu(errors, rawStderr, onRetry) {
2472
2513
  debug("showCheckFailureMenu: %d errors", errors.length);
2473
2514
  let clipboardCopied = false;
@@ -2507,13 +2548,13 @@ async function showCheckFailureMenu(errors, rawStderr, onRetry) {
2507
2548
  debug("showCheckFailureMenu: user chose %s", choice);
2508
2549
  switch (choice) {
2509
2550
  case "copy":
2510
- if (await copyToClipboard(rawStderr)) {
2551
+ if (await copyToClipboard(filterTestOutput(rawStderr))) {
2511
2552
  clipboardCopied = true;
2512
2553
  p$1.log.step(green("Copied to clipboard."));
2513
2554
  } else p$1.log.warn(red("No clipboard tool found. Install xclip, wl-copy, or xsel."));
2514
2555
  continue;
2515
2556
  case "view":
2516
- p$1.note(rawStderr.trim() || "(no raw output)", "Full error output");
2557
+ p$1.note(filterTestOutput(rawStderr) || "(no raw output)", "Full error output");
2517
2558
  continue;
2518
2559
  case "retry":
2519
2560
  if (onRetry) return "retried";
@@ -2528,24 +2569,42 @@ async function showCheckFailureMenu(errors, rawStderr, onRetry) {
2528
2569
  }
2529
2570
  }
2530
2571
  //#endregion
2531
- //#region src/ui/check-summary.ts
2572
+ //#region src/ui/check-progress.ts
2532
2573
  /**
2533
- * Stop a check spinner with a per-tool summary of the check results.
2574
+ * Creates a live check progress display.
2534
2575
  *
2535
- * - On success: stops with "All checks passed" and prints a `โœ“ tool` line
2536
- * for each result.
2537
- * - On failure: stops with "N checks failed" (pluralized). Raw error output
2538
- * is intentionally NOT printed here โ€” callers handle failure display
2539
- * (menu, raw print, etc.).
2576
+ * - Each check gets its own line. While a check runs, a spinner animates
2577
+ * in the status position. When it completes, the spinner becomes a โœ“ or โœ—.
2578
+ * - There is no separate "Running checksโ€ฆ" header โ€” the spinner IS the
2579
+ * check line.
2580
+ * - When all checks finish, a `log.info` summary line is emitted.
2581
+ *
2582
+ * Returns a `CheckObserver` (pass directly to `runAllChecks`) plus a `finish()`
2583
+ * method that prints the final summary.
2540
2584
  */
2541
- function stopCheckSpinner(spinner, results) {
2542
- if (results.ok) {
2543
- spinner.stop("All checks passed");
2544
- if (results.results.length > 0) log.info(results.results.map((r) => ` ${green("โœ“")} ${r.tool}`).join("\n"));
2545
- } else {
2546
- const failed = results.results.filter((r) => !r.ok);
2547
- spinner.stop(`${failed.length} check${failed.length !== 1 ? "s" : ""} failed`);
2548
- }
2585
+ function createCheckProgressDisplay() {
2586
+ const s = spinner();
2587
+ const toolResults = [];
2588
+ let started = false;
2589
+ return {
2590
+ onStart(_tool, _command, _matchedFiles) {
2591
+ if (!started) {
2592
+ s.start(_tool);
2593
+ started = true;
2594
+ } else s.message(_tool);
2595
+ },
2596
+ onResult(result) {
2597
+ toolResults.push({
2598
+ tool: result.tool,
2599
+ ok: result.ok
2600
+ });
2601
+ s.message(`${result.ok ? "โœ“" : "โœ—"} ${result.tool}`);
2602
+ },
2603
+ finish(ok) {
2604
+ s.stop(ok ? green("All checks passed") : red("Some checks failed"));
2605
+ if (toolResults.length > 0) log.info(toolResults.map((r) => ` ${r.ok ? green("โœ“") : red("โœ—")} ${r.tool}`).join("\n"));
2606
+ }
2607
+ };
2549
2608
  }
2550
2609
  //#endregion
2551
2610
  //#region src/commands/check-phase.ts
@@ -2554,7 +2613,7 @@ function stopCheckSpinner(spinner, results) {
2554
2613
  *
2555
2614
  * Single entry point for the check-execution pipeline shared by `runPreCommitChecks`
2556
2615
  * (post-staging, normal commit flow) and `runAutoGroupFlow` (pre-staging, auto-group
2557
- * flow). Encapsulates: detectConfig guard โ†’ spinner โ†’ runAllChecks โ†’ retry loop with
2616
+ * flow). Encapsulates: detectConfig guard โ†’ live progress display โ†’ runAllChecks โ†’ retry loop with
2558
2617
  * `showCheckFailureMenu`.
2559
2618
  *
2560
2619
  * Caller responsibilities:
@@ -2570,10 +2629,9 @@ function stopCheckSpinner(spinner, results) {
2570
2629
  async function runCheckPhaseInteractive(repoRoot, files, timeout, onRetry) {
2571
2630
  if (!await detectConfig(repoRoot)) return "passed";
2572
2631
  debug("Running user checks on %d files...", files.length);
2573
- const ck = spinner();
2574
- ck.start("Running checks...");
2575
- let checkResults = await runAllChecks(repoRoot, files, timeout);
2576
- stopCheckSpinner(ck, checkResults);
2632
+ const display = createCheckProgressDisplay();
2633
+ let checkResults = await runAllChecks(repoRoot, files, timeout, display);
2634
+ display.finish(checkResults.ok);
2577
2635
  debug("Check results: ok=%s, count=%d", checkResults.ok, checkResults.results.length);
2578
2636
  while (!checkResults.ok) {
2579
2637
  const rawOutput = checkResults.results.filter((r) => !r.ok).map((r) => `[${r.tool}]\n${r.stdout}\n${r.stderr}`.trim()).join("\n\n");
@@ -2584,9 +2642,9 @@ async function runCheckPhaseInteractive(repoRoot, files, timeout, onRetry) {
2584
2642
  if (menuResult === "retried") {
2585
2643
  debug("Re-running checks after retry...");
2586
2644
  if (onRetry) await onRetry();
2587
- ck.start("Running checks...");
2588
- checkResults = await runAllChecks(repoRoot, files, timeout);
2589
- stopCheckSpinner(ck, checkResults);
2645
+ const retryDisplay = createCheckProgressDisplay();
2646
+ checkResults = await runAllChecks(repoRoot, files, timeout, retryDisplay);
2647
+ retryDisplay.finish(checkResults.ok);
2590
2648
  debug("Retry check results: ok=%s, count=%d", checkResults.ok, checkResults.results.length);
2591
2649
  continue;
2592
2650
  }
@@ -3570,6 +3628,52 @@ let m = class {
3570
3628
  }
3571
3629
  }
3572
3630
  };
3631
+ let nt = class extends m {
3632
+ options;
3633
+ cursor = 0;
3634
+ get _value() {
3635
+ return this.options[this.cursor].value;
3636
+ }
3637
+ get _enabledOptions() {
3638
+ return this.options.filter((t) => t.disabled !== !0);
3639
+ }
3640
+ toggleAll() {
3641
+ const t = this._enabledOptions, s = this.value !== void 0 && this.value.length === t.length;
3642
+ this.value = s ? [] : t.map((e) => e.value);
3643
+ }
3644
+ toggleInvert() {
3645
+ const t = this.value;
3646
+ if (!t) return;
3647
+ const s = this._enabledOptions.filter((e) => !t.includes(e.value));
3648
+ this.value = s.map((e) => e.value);
3649
+ }
3650
+ toggleValue() {
3651
+ this.value === void 0 && (this.value = []);
3652
+ const t = this.value.includes(this._value);
3653
+ this.value = t ? this.value.filter((s) => s !== this._value) : [...this.value, this._value];
3654
+ }
3655
+ constructor(t) {
3656
+ super(t, !1), this.options = t.options, this.value = [...t.initialValues ?? []];
3657
+ const s = Math.max(this.options.findIndex(({ value: e }) => e === t.cursorAt), 0);
3658
+ this.cursor = this.options[s].disabled ? f(s, 1, this.options) : s, this.on("key", (e) => {
3659
+ e === "a" && this.toggleAll(), e === "i" && this.toggleInvert();
3660
+ }), this.on("cursor", (e) => {
3661
+ switch (e) {
3662
+ case "left":
3663
+ case "up":
3664
+ this.cursor = f(this.cursor, -1, this.options);
3665
+ break;
3666
+ case "down":
3667
+ case "right":
3668
+ this.cursor = f(this.cursor, 1, this.options);
3669
+ break;
3670
+ case "space":
3671
+ this.toggleValue();
3672
+ break;
3673
+ }
3674
+ });
3675
+ }
3676
+ };
3573
3677
  var ut = class extends m {
3574
3678
  options;
3575
3679
  cursor = 0;
@@ -3598,6 +3702,86 @@ var ut = class extends m {
3598
3702
  }
3599
3703
  };
3600
3704
  //#endregion
3705
+ //#region src/ui/file-multiselect.ts
3706
+ /**
3707
+ * Render a checkbox-style option line with 4 visual states:
3708
+ * active-selected, selected, active, inactive
3709
+ */
3710
+ function renderCheckboxOption(label, selected, active) {
3711
+ if (selected && active) return `${green(S_CHECKBOX_SELECTED)} ${label}`;
3712
+ if (selected) return `${dim(S_CHECKBOX_SELECTED)} ${dim(label)}`;
3713
+ if (active) return `${green(S_CHECKBOX_ACTIVE)} ${label}`;
3714
+ return `${dim(S_CHECKBOX_INACTIVE)} ${dim(label)}`;
3715
+ }
3716
+ /**
3717
+ * Build the render function for a MultiSelectPrompt with file-multiselect
3718
+ * styling and a hotkey hint line at the bottom.
3719
+ */
3720
+ function buildMultiSelectRender(message, options, output) {
3721
+ return function() {
3722
+ const header = `${symbol(this.state)} ${message}`;
3723
+ const value = this.value ?? [];
3724
+ const cursor = this.cursor;
3725
+ switch (this.state) {
3726
+ case "submit": {
3727
+ const labels = value.map((v) => {
3728
+ return options.find((o) => o.value === v)?.label ?? String(v);
3729
+ }).join(dim(", "));
3730
+ return `${header}\n${dim(S_BAR)} ${dim(labels)}`;
3731
+ }
3732
+ case "cancel": {
3733
+ const cancelled = styleText(["strikethrough", "dim"], "Cancelled");
3734
+ return `${header}\n${dim(S_BAR_END)} ${cancelled}`;
3735
+ }
3736
+ default: {
3737
+ const lines = limitOptions({
3738
+ cursor,
3739
+ options,
3740
+ style: (opt, active) => renderCheckboxOption(opt.label, value.includes(opt.value), active),
3741
+ maxItems: 7,
3742
+ output: output ?? process.stdout
3743
+ }).map((line) => `${dim(S_BAR)} ${line}`);
3744
+ const hintLine = "โ†‘/โ†“ navigate ยท space select ยท enter confirm ยท A select all ยท N select none";
3745
+ return [
3746
+ header,
3747
+ ...lines,
3748
+ `${dim(S_BAR_END)} ${dim(hintLine)}`
3749
+ ].join("\n");
3750
+ }
3751
+ }
3752
+ };
3753
+ }
3754
+ /**
3755
+ * Multi-select prompt with extended hotkeys for file selection.
3756
+ *
3757
+ * Built-in (from `@clack/core`'s `MultiSelectPrompt`):
3758
+ * `a` โ€” toggle all (select all / deselect all)
3759
+ * `i` โ€” invert selection
3760
+ *
3761
+ * Extended hotkeys:
3762
+ * `A` (shift+A) โ€” select all unconditionally
3763
+ * `N` โ€” select none (deselect all)
3764
+ */
3765
+ async function fileMultiSelect(message, options, opts) {
3766
+ const required = opts?.required ?? true;
3767
+ const prompt = new nt({
3768
+ options,
3769
+ required,
3770
+ input: opts?.input,
3771
+ output: opts?.output,
3772
+ validate: (values) => {
3773
+ if (required && (!values || values.length === 0)) return "Please select at least one option.";
3774
+ },
3775
+ render: buildMultiSelectRender(message, options, opts?.output)
3776
+ });
3777
+ prompt.on("key", (char, key) => {
3778
+ if (!char) return;
3779
+ if (char === "a" && key?.shift) prompt.value = options.map((o) => o.value);
3780
+ if (char === "n") prompt.value = [];
3781
+ });
3782
+ return await prompt.prompt();
3783
+ }
3784
+ //#endregion
3601
3785
  //#region src/ui/toggle-select.ts
3602
3786
  const ON_LABEL = styleText("green", "ON");
3603
3787
  const OFF_LABEL = styleText("dim", "OFF");
@@ -3665,8 +3849,8 @@ function buildPromptRenderer(opts, state) {
3665
3849
  const toggleList = opts.toggles;
3666
3850
  return function() {
3667
3851
  const sym = symbol(this.state);
3668
- const statusLines = toggleList.map((t) => renderToggleState(t, state[t.hotkey])).join("\n");
3669
- const header = `${sym} ${opts.message}\n${dim(S_BAR)} ${statusLines}`;
3852
+ const statusLines = toggleList.map((t) => `${dim(S_BAR)} ${renderToggleState(t, state[t.hotkey])}`).join("\n");
3853
+ const header = `${sym} ${opts.message}\n${statusLines}`;
3670
3854
  switch (this.state) {
3671
3855
  case "submit": {
3672
3856
  const selected = optionList[this.cursor];
@@ -3795,14 +3979,10 @@ async function showStagingMenu(files, hasChecks) {
3795
3979
  files: files.map((f) => f.path),
3796
3980
  all: true
3797
3981
  };
3798
- const selected = await p$1.multiselect({
3799
- message: "Select files to stage:",
3800
- options: sorted.map((f) => ({
3801
- label: `${statusLabel(f.status)} ${f.path}`,
3802
- value: f.path
3803
- })),
3804
- required: true
3805
- });
3982
+ const selected = await fileMultiSelect("Select files to stage:", sorted.map((f) => ({
3983
+ label: `${statusLabel(f.status)} ${f.path}`,
3984
+ value: f.path
3985
+ })), { required: true });
3806
3986
  if (p$1.isCancel(selected)) return null;
3807
3987
  return {
3808
3988
  files: selected,
@@ -3835,10 +4015,9 @@ async function handleStaging(changedFiles, flags) {
3835
4015
  await stageAll();
3836
4016
  const allFiles = await getStagedFiles();
3837
4017
  if (await detectConfig(repoRoot)) {
3838
- const ckSpinner = spinner();
3839
- ckSpinner.start("Running checks...");
3840
- const ckResult = await runAllChecks(repoRoot, allFiles, 6e4);
3841
- stopCheckSpinner(ckSpinner, ckResult);
4018
+ const display = createCheckProgressDisplay();
4019
+ const ckResult = await runAllChecks(repoRoot, allFiles, 6e4, display);
4020
+ display.finish(ckResult.ok);
3842
4021
  if (!ckResult.ok) for (const r of ckResult.results.filter((r) => !r.ok)) log.info(r.stderr?.trim() || r.stdout?.trim() || `Check failed: ${r.command}`);
3843
4022
  }
3844
4023
  currentFiles = await getChangedFiles();