@kyubiware/commit-mint 0.6.1 → 0.6.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/cli.mjs CHANGED
@@ -28,7 +28,7 @@ var __exportAll = (all, no_symbols) => {
28
28
  //#region package.json
29
29
  var package_default = {
30
30
  name: "@kyubiware/commit-mint",
31
- version: "0.6.1",
31
+ version: "0.6.3",
32
32
  description: "🌿 A commit tool that actually handles hook failures",
33
33
  type: "module",
34
34
  bin: { "cmint": "./dist/cli.mjs" },
@@ -39,7 +39,7 @@ var package_default = {
39
39
  "dev:auto": "tsx src/cli.ts -a",
40
40
  "dev:debug": "tsx src/cli.ts --debug",
41
41
  "lint": "biome check .",
42
- "lint:fix": "biome check --fix .",
42
+ "lint:fix": "biome check --write --unsafe .",
43
43
  "typecheck": "tsc --noEmit",
44
44
  "test": "vitest run",
45
45
  "test:coverage": "vitest run --coverage",
@@ -1061,6 +1061,92 @@ async function attemptCommitNoVerify(message, onProgress) {
1061
1061
  return attemptCommit(message, ["--no-verify"], onProgress);
1062
1062
  }
1063
1063
  //#endregion
1064
+ //#region src/services/grouping-parser.ts
1065
+ /** Coerce a parsed value into a CommitGroup, or null if it doesn't match the shape. */
1066
+ function coerceGroup(item) {
1067
+ if (typeof item === "object" && item !== null && "name" in item && "description" in item && "files" in item && Array.isArray(item.files)) {
1068
+ const obj = item;
1069
+ return {
1070
+ name: String(obj.name),
1071
+ description: String(obj.description),
1072
+ files: obj.files.filter((f) => typeof f === "string")
1073
+ };
1074
+ }
1075
+ return null;
1076
+ }
1077
+ /** Return the index of the closing quote of a string starting at text[start] === '"'. */
1078
+ function skipString(text, start) {
1079
+ let i = start + 1;
1080
+ while (i < text.length) {
1081
+ const ch = text[i];
1082
+ if (ch === "\\") {
1083
+ i += 2;
1084
+ continue;
1085
+ }
1086
+ if (ch === "\"") return i;
1087
+ i++;
1088
+ }
1089
+ return i;
1090
+ }
1091
+ function openBrace(state, index) {
1092
+ if (state.depth === 0) state.start = index;
1093
+ state.depth++;
1094
+ }
1095
+ function pushParsedObject(objects, candidate) {
1096
+ try {
1097
+ objects.push(JSON.parse(candidate));
1098
+ } catch {}
1099
+ }
1100
+ function closeBrace(state, text, index) {
1101
+ if (state.depth === 0) return;
1102
+ state.depth--;
1103
+ if (state.depth === 0 && state.start !== -1) {
1104
+ pushParsedObject(state.objects, text.slice(state.start, index + 1));
1105
+ state.start = -1;
1106
+ }
1107
+ }
1108
+ /**
1109
+ * Scan for top-level `{...}` objects, respecting string literals and escapes.
1110
+ * Used to recover groups when the model emits a single object or concatenated
1111
+ * objects instead of the requested JSON array.
1112
+ */
1113
+ function extractTopLevelObjects(text) {
1114
+ const state = {
1115
+ objects: [],
1116
+ depth: 0,
1117
+ start: -1
1118
+ };
1119
+ for (let i = 0; i < text.length; i++) {
1120
+ const ch = text[i];
1121
+ if (ch === "\"") {
1122
+ i = skipString(text, i);
1123
+ continue;
1124
+ }
1125
+ if (ch === "{") {
1126
+ openBrace(state, i);
1127
+ continue;
1128
+ }
1129
+ if (ch === "}") closeBrace(state, text, i);
1130
+ }
1131
+ return state.objects;
1132
+ }
1133
+ function parseGroupingResponse(content) {
1134
+ let cleaned = content.replace(/<think[\s\S]*?<\/think>/gi, "").trim();
1135
+ cleaned = cleaned.replace(/^```(?:json)?\s*/i, "").replace(/\s*```$/i, "").trim();
1136
+ const start = cleaned.indexOf("[");
1137
+ const end = cleaned.lastIndexOf("]");
1138
+ if (start !== -1 && end !== -1 && end > start) try {
1139
+ const parsed = JSON.parse(cleaned.slice(start, end + 1));
1140
+ if (Array.isArray(parsed)) {
1141
+ const groups = parsed.map(coerceGroup).filter((g) => g !== null);
1142
+ if (groups.length > 0) return groups;
1143
+ }
1144
+ } catch {}
1145
+ const groups = extractTopLevelObjects(cleaned).map(coerceGroup).filter((g) => g !== null);
1146
+ if (groups.length > 0) return groups;
1147
+ throw new Error("AI response did not contain a JSON array");
1148
+ }
1149
+ //#endregion
1064
1150
  //#region src/services/grouping.ts
1065
1151
  function matchesExcludePattern(filePath, pattern) {
1066
1152
  if (pattern === filePath) return true;
@@ -1165,23 +1251,6 @@ function buildRetryGroupingPrompt() {
1165
1251
  "Output ONLY valid JSON. No markdown fences, no explanation."
1166
1252
  ].join("\n");
1167
1253
  }
1168
- function parseGroupingResponse(content) {
1169
- let cleaned = content.replace(/<think[\s\S]*?<\/think>/gi, "").trim();
1170
- cleaned = cleaned.replace(/^```(?:json)?\s*/i, "").replace(/\s*```$/i, "").trim();
1171
- const start = cleaned.indexOf("[");
1172
- const end = cleaned.lastIndexOf("]");
1173
- if (start === -1 || end === -1 || end <= start) throw new Error("AI response did not contain a JSON array");
1174
- const jsonText = cleaned.slice(start, end + 1);
1175
- const parsed = JSON.parse(jsonText);
1176
- if (!Array.isArray(parsed)) throw new Error("AI response was not a JSON array");
1177
- const rawGroups = [];
1178
- for (const item of parsed) if (typeof item === "object" && item !== null && "name" in item && "description" in item && "files" in item && Array.isArray(item.files)) rawGroups.push({
1179
- name: String(item.name),
1180
- description: String(item.description),
1181
- files: item.files.filter((f) => typeof f === "string")
1182
- });
1183
- return rawGroups;
1184
- }
1185
1254
  async function generateGroups(files, apiKey, model, timeout, provider, proxy) {
1186
1255
  debug("generateGroups: %d files, model=%s", files.length, model ?? "default");
1187
1256
  const { included, excluded } = filterExcludedFiles(files);
@@ -1544,6 +1613,26 @@ async function showCheckFailureMenu(errors, rawStderr, onRetry) {
1544
1613
  }
1545
1614
  }
1546
1615
  //#endregion
1616
+ //#region src/ui/check-summary.ts
1617
+ /**
1618
+ * Stop a check spinner with a per-tool summary of the check results.
1619
+ *
1620
+ * - On success: stops with "All checks passed" and prints a `✓ tool` line
1621
+ * for each result.
1622
+ * - On failure: stops with "N checks failed" (pluralized). Raw error output
1623
+ * is intentionally NOT printed here — callers handle failure display
1624
+ * (menu, raw print, etc.).
1625
+ */
1626
+ function stopCheckSpinner(spinner, results) {
1627
+ if (results.ok) {
1628
+ spinner.stop("All checks passed");
1629
+ if (results.results.length > 0) log.info(results.results.map((r) => ` ${green("✓")} ${r.tool}`).join("\n"));
1630
+ } else {
1631
+ const failed = results.results.filter((r) => !r.ok);
1632
+ spinner.stop(`${failed.length} check${failed.length !== 1 ? "s" : ""} failed`);
1633
+ }
1634
+ }
1635
+ //#endregion
1547
1636
  //#region src/ui/grouping.ts
1548
1637
  async function showGroupingConfirmation(groups, excluded) {
1549
1638
  debug("showGroupingConfirmation: %d groups, %d excluded", groups.length, excluded.length);
@@ -1805,10 +1894,7 @@ async function runAutoGroupFlow(changedFiles, flags) {
1805
1894
  }
1806
1895
  break;
1807
1896
  }
1808
- if (checkResults.ok) {
1809
- ck.stop("All checks passed");
1810
- if (checkResults.results.length > 0) log.info(checkResults.results.map((r) => ` ${green("✓")} ${r.tool}`).join("\n"));
1811
- }
1897
+ if (checkResults.ok) stopCheckSpinner(ck, checkResults);
1812
1898
  }
1813
1899
  }
1814
1900
  const config = await readConfig();
@@ -2526,14 +2612,8 @@ async function handleStaging(changedFiles, flags) {
2526
2612
  const ckSpinner = spinner();
2527
2613
  ckSpinner.start("Running checks...");
2528
2614
  const ckResult = await runAllChecks(repoRoot, allFiles, 6e4);
2529
- if (ckResult.ok) {
2530
- ckSpinner.stop("All checks passed");
2531
- for (const r of ckResult.results) if (r.stdout.trim()) log.info(dim(r.stdout.trim()));
2532
- } else {
2533
- const failed = ckResult.results.filter((r) => !r.ok);
2534
- ckSpinner.stop(`${failed.length} check${failed.length !== 1 ? "s" : ""} failed`);
2535
- for (const r of failed) log.info(r.stderr?.trim() || r.stdout?.trim() || `Check failed: ${r.command}`);
2536
- }
2615
+ stopCheckSpinner(ckSpinner, ckResult);
2616
+ 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}`);
2537
2617
  }
2538
2618
  currentFiles = await getChangedFiles();
2539
2619
  continue;
@@ -2569,7 +2649,10 @@ async function runPreCommitChecks(changedFiles, noCheck) {
2569
2649
  const stagedFileList = changedFiles.filter((f) => f.staged && f.status !== "D").map((f) => f.path);
2570
2650
  if (stagedFileList.length === 0) return;
2571
2651
  debug("Running user checks on %d staged files...", stagedFileList.length);
2652
+ const ckSpinner = spinner();
2653
+ ckSpinner.start("Running checks...");
2572
2654
  let checkResults = await runAllChecks(checkRoot, stagedFileList, 6e4);
2655
+ stopCheckSpinner(ckSpinner, checkResults);
2573
2656
  debug("Check results: ok=%s, count=%d", checkResults.ok, checkResults.results.length);
2574
2657
  while (!checkResults.ok) {
2575
2658
  const rawOutput = checkResults.results.filter((r) => !r.ok).map((r) => `[${r.tool}]\n${r.stdout}\n${r.stderr}`.trim()).join("\n\n");
@@ -2584,13 +2667,7 @@ async function runPreCommitChecks(changedFiles, noCheck) {
2584
2667
  ckSpinner.start("Running checks...");
2585
2668
  checkResults = await runAllChecks(checkRoot, stagedFileList, 6e4);
2586
2669
  debug("Retry check results: ok=%s, count=%d", checkResults.ok, checkResults.results.length);
2587
- if (checkResults.ok) {
2588
- ckSpinner.stop("All checks passed");
2589
- for (const r of checkResults.results) if (r.stdout.trim()) log.info(dim(r.stdout.trim()));
2590
- } else {
2591
- const retryFailed = checkResults.results.filter((r) => !r.ok);
2592
- ckSpinner.stop(`${retryFailed.length} check${retryFailed.length !== 1 ? "s" : ""} failed`);
2593
- }
2670
+ stopCheckSpinner(ckSpinner, checkResults);
2594
2671
  continue;
2595
2672
  }
2596
2673
  break;