@kyubiware/commit-mint 0.9.3 β†’ 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
@@ -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.3",
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");
@@ -2543,24 +2569,42 @@ async function showCheckFailureMenu(errors, rawStderr, onRetry) {
2543
2569
  }
2544
2570
  }
2545
2571
  //#endregion
2546
- //#region src/ui/check-summary.ts
2572
+ //#region src/ui/check-progress.ts
2547
2573
  /**
2548
- * Stop a check spinner with a per-tool summary of the check results.
2574
+ * Creates a live check progress display.
2575
+ *
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.
2549
2581
  *
2550
- * - On success: stops with "All checks passed" and prints a `βœ“ tool` line
2551
- * for each result.
2552
- * - On failure: stops with "N checks failed" (pluralized). Raw error output
2553
- * is intentionally NOT printed here β€” callers handle failure display
2554
- * (menu, raw print, etc.).
2582
+ * Returns a `CheckObserver` (pass directly to `runAllChecks`) plus a `finish()`
2583
+ * method that prints the final summary.
2555
2584
  */
2556
- function stopCheckSpinner(spinner, results) {
2557
- if (results.ok) {
2558
- spinner.stop("All checks passed");
2559
- if (results.results.length > 0) log.info(results.results.map((r) => ` ${green("βœ“")} ${r.tool}`).join("\n"));
2560
- } else {
2561
- const failed = results.results.filter((r) => !r.ok);
2562
- spinner.stop(`${failed.length} check${failed.length !== 1 ? "s" : ""} failed`);
2563
- }
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
+ };
2564
2608
  }
2565
2609
  //#endregion
2566
2610
  //#region src/commands/check-phase.ts
@@ -2569,7 +2613,7 @@ function stopCheckSpinner(spinner, results) {
2569
2613
  *
2570
2614
  * Single entry point for the check-execution pipeline shared by `runPreCommitChecks`
2571
2615
  * (post-staging, normal commit flow) and `runAutoGroupFlow` (pre-staging, auto-group
2572
- * flow). Encapsulates: detectConfig guard β†’ spinner β†’ runAllChecks β†’ retry loop with
2616
+ * flow). Encapsulates: detectConfig guard β†’ live progress display β†’ runAllChecks β†’ retry loop with
2573
2617
  * `showCheckFailureMenu`.
2574
2618
  *
2575
2619
  * Caller responsibilities:
@@ -2585,10 +2629,9 @@ function stopCheckSpinner(spinner, results) {
2585
2629
  async function runCheckPhaseInteractive(repoRoot, files, timeout, onRetry) {
2586
2630
  if (!await detectConfig(repoRoot)) return "passed";
2587
2631
  debug("Running user checks on %d files...", files.length);
2588
- const ck = spinner();
2589
- ck.start("Running checks...");
2590
- let checkResults = await runAllChecks(repoRoot, files, timeout);
2591
- stopCheckSpinner(ck, checkResults);
2632
+ const display = createCheckProgressDisplay();
2633
+ let checkResults = await runAllChecks(repoRoot, files, timeout, display);
2634
+ display.finish(checkResults.ok);
2592
2635
  debug("Check results: ok=%s, count=%d", checkResults.ok, checkResults.results.length);
2593
2636
  while (!checkResults.ok) {
2594
2637
  const rawOutput = checkResults.results.filter((r) => !r.ok).map((r) => `[${r.tool}]\n${r.stdout}\n${r.stderr}`.trim()).join("\n\n");
@@ -2599,9 +2642,9 @@ async function runCheckPhaseInteractive(repoRoot, files, timeout, onRetry) {
2599
2642
  if (menuResult === "retried") {
2600
2643
  debug("Re-running checks after retry...");
2601
2644
  if (onRetry) await onRetry();
2602
- ck.start("Running checks...");
2603
- checkResults = await runAllChecks(repoRoot, files, timeout);
2604
- stopCheckSpinner(ck, checkResults);
2645
+ const retryDisplay = createCheckProgressDisplay();
2646
+ checkResults = await runAllChecks(repoRoot, files, timeout, retryDisplay);
2647
+ retryDisplay.finish(checkResults.ok);
2605
2648
  debug("Retry check results: ok=%s, count=%d", checkResults.ok, checkResults.results.length);
2606
2649
  continue;
2607
2650
  }
@@ -3806,8 +3849,8 @@ function buildPromptRenderer(opts, state) {
3806
3849
  const toggleList = opts.toggles;
3807
3850
  return function() {
3808
3851
  const sym = symbol(this.state);
3809
- const statusLines = toggleList.map((t) => renderToggleState(t, state[t.hotkey])).join("\n");
3810
- 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}`;
3811
3854
  switch (this.state) {
3812
3855
  case "submit": {
3813
3856
  const selected = optionList[this.cursor];
@@ -3972,10 +4015,9 @@ async function handleStaging(changedFiles, flags) {
3972
4015
  await stageAll();
3973
4016
  const allFiles = await getStagedFiles();
3974
4017
  if (await detectConfig(repoRoot)) {
3975
- const ckSpinner = spinner();
3976
- ckSpinner.start("Running checks...");
3977
- const ckResult = await runAllChecks(repoRoot, allFiles, 6e4);
3978
- stopCheckSpinner(ckSpinner, ckResult);
4018
+ const display = createCheckProgressDisplay();
4019
+ const ckResult = await runAllChecks(repoRoot, allFiles, 6e4, display);
4020
+ display.finish(ckResult.ok);
3979
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}`);
3980
4022
  }
3981
4023
  currentFiles = await getChangedFiles();