@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 +115 -38
- package/dist/cli.mjs.map +1 -1
- package/package.json +2 -2
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.
|
|
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 --
|
|
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
|
-
|
|
2530
|
-
|
|
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
|
-
|
|
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;
|