@kyubiware/commit-mint 0.8.0 → 0.8.1
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 +381 -337
- package/dist/cli.mjs.map +1 -1
- package/package.json +1 -1
package/dist/cli.mjs
CHANGED
|
@@ -29,7 +29,7 @@ var __exportAll = (all, no_symbols) => {
|
|
|
29
29
|
//#region package.json
|
|
30
30
|
var package_default = {
|
|
31
31
|
name: "@kyubiware/commit-mint",
|
|
32
|
-
version: "0.8.
|
|
32
|
+
version: "0.8.1",
|
|
33
33
|
description: "🌿 AI-powered git commit tool — auto-group changed files, generate messages, run pre-commit checks",
|
|
34
34
|
type: "module",
|
|
35
35
|
bin: { "cmint": "./dist/cli.mjs" },
|
|
@@ -673,7 +673,10 @@ async function runCommand(command, timeout, repoRoot) {
|
|
|
673
673
|
timeout,
|
|
674
674
|
all: true,
|
|
675
675
|
preferLocal: true,
|
|
676
|
-
...repoRoot ? {
|
|
676
|
+
...repoRoot ? {
|
|
677
|
+
localDir: repoRoot,
|
|
678
|
+
cwd: repoRoot
|
|
679
|
+
} : {}
|
|
677
680
|
});
|
|
678
681
|
const ok = !result.failed;
|
|
679
682
|
debug("runCommand: %s — ok=%s", tool, ok);
|
|
@@ -924,8 +927,10 @@ var git_exports = /* @__PURE__ */ __exportAll({
|
|
|
924
927
|
getHead: () => getHead,
|
|
925
928
|
getRepoRoot: () => getRepoRoot,
|
|
926
929
|
getStagedDiff: () => getStagedDiff,
|
|
930
|
+
getStagedFiles: () => getStagedFiles,
|
|
927
931
|
getStatusShort: () => getStatusShort,
|
|
928
932
|
resetStaging: () => resetStaging,
|
|
933
|
+
resolveToRepoRoot: () => resolveToRepoRoot,
|
|
929
934
|
stageAll: () => stageAll,
|
|
930
935
|
stageFiles: () => stageFiles
|
|
931
936
|
});
|
|
@@ -1043,6 +1048,44 @@ async function getChangedFiles() {
|
|
|
1043
1048
|
debug("getChangedFiles:", files.length, "files");
|
|
1044
1049
|
return files;
|
|
1045
1050
|
}
|
|
1051
|
+
/**
|
|
1052
|
+
* Return staged file paths relative to the repository root, excluding deletions.
|
|
1053
|
+
*
|
|
1054
|
+
* `git status --short` reports paths relative to the current working directory,
|
|
1055
|
+
* but `.cmintrc` globs are written from the repo root (matching lint-staged
|
|
1056
|
+
* conventions). Use this helper whenever staged paths need to match repo-root
|
|
1057
|
+
* globs. `--diff-filter=d` excludes staged deletions so check commands don't
|
|
1058
|
+
* receive paths whose content no longer exists.
|
|
1059
|
+
*/
|
|
1060
|
+
async function getStagedFiles() {
|
|
1061
|
+
const { stdout } = await execa("git", [
|
|
1062
|
+
"diff",
|
|
1063
|
+
"--cached",
|
|
1064
|
+
"--name-only",
|
|
1065
|
+
"--diff-filter=d"
|
|
1066
|
+
]);
|
|
1067
|
+
const files = stdout.split("\n").map((line) => line.trim()).filter(Boolean);
|
|
1068
|
+
debug("getStagedFiles:", files.length, "files");
|
|
1069
|
+
return files;
|
|
1070
|
+
}
|
|
1071
|
+
/**
|
|
1072
|
+
* Convert cwd-relative file paths to repo-root-relative paths.
|
|
1073
|
+
*
|
|
1074
|
+
* Uses `git rev-parse --show-prefix` to discover the prefix of the current
|
|
1075
|
+
* working directory relative to the repo root (e.g. `"extension/"` when cwd
|
|
1076
|
+
* is `<repo>/extension`, or `""` when at the repo root). Useful when a caller
|
|
1077
|
+
* has cwd-relative paths from `getChangedFiles()` but needs to match them
|
|
1078
|
+
* against repo-root-relative `.cmintrc` globs (e.g. the auto-group flow,
|
|
1079
|
+
* which runs checks BEFORE files are staged — so `getStagedFiles()` can't be
|
|
1080
|
+
* used because the index doesn't yet contain those paths).
|
|
1081
|
+
*/
|
|
1082
|
+
async function resolveToRepoRoot(cwdRelativePaths) {
|
|
1083
|
+
if (cwdRelativePaths.length === 0) return [];
|
|
1084
|
+
const { stdout } = await execa("git", ["rev-parse", "--show-prefix"]);
|
|
1085
|
+
const prefix = stdout.trim();
|
|
1086
|
+
if (!prefix) return [...cwdRelativePaths];
|
|
1087
|
+
return cwdRelativePaths.map((p) => `${prefix}${p}`);
|
|
1088
|
+
}
|
|
1046
1089
|
async function stageFiles(paths) {
|
|
1047
1090
|
debug("stageFiles:", paths);
|
|
1048
1091
|
await execa("git", ["add", ...paths]);
|
|
@@ -1586,6 +1629,68 @@ async function loadCachedCommit(repoPath) {
|
|
|
1586
1629
|
}
|
|
1587
1630
|
}
|
|
1588
1631
|
//#endregion
|
|
1632
|
+
//#region src/ui/grouping.ts
|
|
1633
|
+
async function showGroupingConfirmation(groups, excluded) {
|
|
1634
|
+
debug("showGroupingConfirmation: %d groups, %d excluded", groups.length, excluded.length);
|
|
1635
|
+
const lines = [];
|
|
1636
|
+
for (const group of groups) {
|
|
1637
|
+
lines.push(bold(group.name));
|
|
1638
|
+
lines.push(` ${dim(group.description)}`);
|
|
1639
|
+
lines.push(` ${green(String(group.files.length))} file${group.files.length !== 1 ? "s" : ""}`);
|
|
1640
|
+
for (const file of group.files) lines.push(` ${dim("•")} ${file}`);
|
|
1641
|
+
lines.push("");
|
|
1642
|
+
}
|
|
1643
|
+
if (excluded.length > 0) {
|
|
1644
|
+
lines.push(dim(`Excluded: ${excluded.length} file${excluded.length !== 1 ? "s" : ""}`));
|
|
1645
|
+
for (const file of excluded) lines.push(` ${dim("•")} ${dim(file)}`);
|
|
1646
|
+
}
|
|
1647
|
+
p.note(lines.join("\n"), "Proposed commit groups");
|
|
1648
|
+
const choice = await p.select({
|
|
1649
|
+
message: "Proceed with these groupings?",
|
|
1650
|
+
options: [{
|
|
1651
|
+
label: "Yes, commit all groups",
|
|
1652
|
+
value: "yes"
|
|
1653
|
+
}, {
|
|
1654
|
+
label: "No, cancel",
|
|
1655
|
+
value: "no"
|
|
1656
|
+
}]
|
|
1657
|
+
});
|
|
1658
|
+
if (p.isCancel(choice) || choice === "no") {
|
|
1659
|
+
debug("showGroupingConfirmation: user cancelled");
|
|
1660
|
+
return false;
|
|
1661
|
+
}
|
|
1662
|
+
debug("showGroupingConfirmation: user confirmed");
|
|
1663
|
+
return true;
|
|
1664
|
+
}
|
|
1665
|
+
function showGroupProgress(current, total, groupName) {
|
|
1666
|
+
p.log.info(`Commit group ${current} of ${total}: ${cyan(`"${groupName}"`)}`);
|
|
1667
|
+
}
|
|
1668
|
+
const statusLabel = (status) => {
|
|
1669
|
+
switch (status) {
|
|
1670
|
+
case "M": return yellow("M");
|
|
1671
|
+
case "A": return green("A");
|
|
1672
|
+
case "D": return red("D");
|
|
1673
|
+
case "?":
|
|
1674
|
+
case "??": return cyan("?");
|
|
1675
|
+
default: return dim(status);
|
|
1676
|
+
}
|
|
1677
|
+
};
|
|
1678
|
+
/** Display combined view: files with status indicators grouped by commit group */
|
|
1679
|
+
function showGroupedFiles(groups, changedFiles) {
|
|
1680
|
+
const statusMap = new Map(changedFiles.map((f) => [f.path, f.status]));
|
|
1681
|
+
const lines = [];
|
|
1682
|
+
for (let i = 0; i < groups.length; i++) {
|
|
1683
|
+
const group = groups[i];
|
|
1684
|
+
lines.push(`${bold(group.name)} ${dim("—")} ${group.files.length} file${group.files.length !== 1 ? "s" : ""}`);
|
|
1685
|
+
for (const file of group.files) {
|
|
1686
|
+
const status = statusMap.get(file) ?? "M";
|
|
1687
|
+
lines.push(` ${statusLabel(status)} ${file}`);
|
|
1688
|
+
}
|
|
1689
|
+
if (i < groups.length - 1) lines.push("");
|
|
1690
|
+
}
|
|
1691
|
+
p.note(lines.join("\n"), "Commit groups");
|
|
1692
|
+
}
|
|
1693
|
+
//#endregion
|
|
1589
1694
|
//#region src/services/clipboard.ts
|
|
1590
1695
|
/** Milliseconds to wait after stdin closes for quick exit failures. */
|
|
1591
1696
|
const GRACE_PERIOD_MS = 150;
|
|
@@ -1650,286 +1755,18 @@ function tryCopy(cmd, args, content) {
|
|
|
1650
1755
|
});
|
|
1651
1756
|
let exitCode = null;
|
|
1652
1757
|
child.on("exit", (code) => {
|
|
1653
|
-
exitCode = code;
|
|
1654
|
-
});
|
|
1655
|
-
child.stdin.write(content, (err) => {
|
|
1656
|
-
if (err) {
|
|
1657
|
-
done(false, "stdin write error");
|
|
1658
|
-
return;
|
|
1659
|
-
}
|
|
1660
|
-
child.stdin.end(() => {
|
|
1661
|
-
setTimeout(() => handleGracePeriod(settled, exitCode, stderrChunks, child, done), GRACE_PERIOD_MS);
|
|
1662
|
-
});
|
|
1663
|
-
});
|
|
1664
|
-
});
|
|
1665
|
-
}
|
|
1666
|
-
//#endregion
|
|
1667
|
-
//#region src/ui/check-failure-menu.ts
|
|
1668
|
-
const MAX_TSC_DIAGNOSTICS = 3;
|
|
1669
|
-
const MAX_ESLINT_DIAGNOSTICS = 3;
|
|
1670
|
-
const MAX_TEST_FAILURES = 3;
|
|
1671
|
-
const MAX_SUMMARY_LINE_LENGTH = 120;
|
|
1672
|
-
const TSC_DIAGNOSTIC = /^(.+?\.(?:ts|tsx|mts|cts|js|jsx|mjs|cjs))\((\d+),(\d+)\):\s+error\s+(TS\d+):\s+(.+)$/;
|
|
1673
|
-
const ESLINT_ERROR_LINE = /^\s*(\d+):(\d+)\s+(error|warning)\s+(.+)\s{2,}(\S+)\s*$/;
|
|
1674
|
-
const TEST_FILE_FAIL = /^\s*FAIL\s+(.+?\.(?:test|spec)\.[^\s>]+)\s*>\s*(.+)$/;
|
|
1675
|
-
function formatCheckFailureSummary(errors) {
|
|
1676
|
-
if (errors.length === 0) return "No check error details were parsed. View full output for details.";
|
|
1677
|
-
return errors.map((error) => formatCheckErrorSummary(error)).join("\n");
|
|
1678
|
-
}
|
|
1679
|
-
function formatCheckErrorSummary(error) {
|
|
1680
|
-
if (error.tool === "tsc") {
|
|
1681
|
-
const diagnostics = extractTscDiagnostics(error.raw || error.message);
|
|
1682
|
-
if (diagnostics.length > 0) return formatTscSummary(diagnostics);
|
|
1683
|
-
}
|
|
1684
|
-
if (error.tool === "eslint") {
|
|
1685
|
-
const diagnostics = extractEslintDiagnostics(error.raw || error.message);
|
|
1686
|
-
if (diagnostics.length > 0) return formatEslintSummary(diagnostics);
|
|
1687
|
-
}
|
|
1688
|
-
if (error.tool === "vitest" || error.tool === "jest") {
|
|
1689
|
-
const failures = extractTestFailures(error.raw || error.message);
|
|
1690
|
-
if (failures.length > 0) return formatTestFailureSummary(failures, error.tool);
|
|
1691
|
-
}
|
|
1692
|
-
const message = firstMeaningfulLine(error.message || error.raw);
|
|
1693
|
-
return ` ${red("•")} [${error.tool}] ${truncate(message, MAX_SUMMARY_LINE_LENGTH)}`;
|
|
1694
|
-
}
|
|
1695
|
-
function extractTscDiagnostics(raw) {
|
|
1696
|
-
return raw.split("\n").map((line) => line.trim()).map((line) => {
|
|
1697
|
-
const match = TSC_DIAGNOSTIC.exec(line);
|
|
1698
|
-
if (!match) return null;
|
|
1699
|
-
return {
|
|
1700
|
-
file: match[1] ?? "",
|
|
1701
|
-
line: match[2] ?? "",
|
|
1702
|
-
column: match[3] ?? "",
|
|
1703
|
-
code: match[4] ?? "",
|
|
1704
|
-
message: match[5] ?? ""
|
|
1705
|
-
};
|
|
1706
|
-
}).filter((diagnostic) => diagnostic !== null);
|
|
1707
|
-
}
|
|
1708
|
-
function formatTscSummary(diagnostics) {
|
|
1709
|
-
const visible = diagnostics.slice(0, MAX_TSC_DIAGNOSTICS);
|
|
1710
|
-
const hidden = diagnostics.length - visible.length;
|
|
1711
|
-
const lines = [` ${red("•")} [tsc] ${diagnostics.length} TypeScript error${diagnostics.length !== 1 ? "s" : ""}`, ...visible.map((diagnostic) => `${diagnostic.file}:${diagnostic.line}:${diagnostic.column} — error ${diagnostic.code}: ${truncate(diagnostic.message, MAX_SUMMARY_LINE_LENGTH)}`)];
|
|
1712
|
-
if (hidden > 0) lines.push(dim(` +${hidden} more TypeScript error${hidden !== 1 ? "s" : ""}. View full output for details.`));
|
|
1713
|
-
return lines.join("\n");
|
|
1714
|
-
}
|
|
1715
|
-
function extractEslintDiagnostics(raw) {
|
|
1716
|
-
const diagnostics = [];
|
|
1717
|
-
const lines = raw.split("\n");
|
|
1718
|
-
let currentFile = "";
|
|
1719
|
-
for (const line of lines) {
|
|
1720
|
-
if (!/^\s/.test(line) && line.includes("/") && !ESLINT_ERROR_LINE.test(line)) {
|
|
1721
|
-
currentFile = line.trim();
|
|
1722
|
-
continue;
|
|
1723
|
-
}
|
|
1724
|
-
const match = ESLINT_ERROR_LINE.exec(line);
|
|
1725
|
-
if (match) diagnostics.push({
|
|
1726
|
-
file: currentFile || "unknown",
|
|
1727
|
-
line: match[1] ?? "",
|
|
1728
|
-
column: match[2] ?? "",
|
|
1729
|
-
severity: match[3] ?? "",
|
|
1730
|
-
message: (match[4] ?? "").trim(),
|
|
1731
|
-
rule: match[5] ?? ""
|
|
1732
|
-
});
|
|
1733
|
-
}
|
|
1734
|
-
return diagnostics;
|
|
1735
|
-
}
|
|
1736
|
-
function extractTestFailures(raw) {
|
|
1737
|
-
const failures = [];
|
|
1738
|
-
const seen = /* @__PURE__ */ new Set();
|
|
1739
|
-
for (const line of raw.split("\n")) {
|
|
1740
|
-
const match = TEST_FILE_FAIL.exec(line);
|
|
1741
|
-
if (!match) continue;
|
|
1742
|
-
const file = (match[1] ?? "").trim();
|
|
1743
|
-
const name = (match[2] ?? "").trim();
|
|
1744
|
-
if (!file || !name) continue;
|
|
1745
|
-
const key = `${file}\u0000${name}`;
|
|
1746
|
-
if (seen.has(key)) continue;
|
|
1747
|
-
seen.add(key);
|
|
1748
|
-
failures.push({
|
|
1749
|
-
file,
|
|
1750
|
-
name
|
|
1751
|
-
});
|
|
1752
|
-
}
|
|
1753
|
-
return failures;
|
|
1754
|
-
}
|
|
1755
|
-
function formatTestFailureSummary(failures, tool) {
|
|
1756
|
-
const total = failures.length;
|
|
1757
|
-
const visible = failures.slice(0, MAX_TEST_FAILURES);
|
|
1758
|
-
const hidden = total - visible.length;
|
|
1759
|
-
const fileCount = new Set(failures.map((f) => f.file)).size;
|
|
1760
|
-
const testNoun = total === 1 ? "test" : "tests";
|
|
1761
|
-
const fileNoun = fileCount === 1 ? "file" : "files";
|
|
1762
|
-
const lines = [` ${red("•")} [${tool}] ${total} failed ${testNoun} in ${fileCount} ${fileNoun}`];
|
|
1763
|
-
const byFile = /* @__PURE__ */ new Map();
|
|
1764
|
-
for (const failure of visible) {
|
|
1765
|
-
const names = byFile.get(failure.file) ?? [];
|
|
1766
|
-
names.push(failure.name);
|
|
1767
|
-
byFile.set(failure.file, names);
|
|
1768
|
-
}
|
|
1769
|
-
for (const [file, names] of byFile) {
|
|
1770
|
-
lines.push(` ${truncate(file, MAX_SUMMARY_LINE_LENGTH)}`);
|
|
1771
|
-
for (const name of names) lines.push(` ${red("×")} ${truncate(name, MAX_SUMMARY_LINE_LENGTH)}`);
|
|
1772
|
-
}
|
|
1773
|
-
if (hidden > 0) lines.push(dim(` +${hidden} more failed ${hidden === 1 ? "test" : "tests"}. View full output for details.`));
|
|
1774
|
-
return lines.join("\n");
|
|
1775
|
-
}
|
|
1776
|
-
function formatEslintSummary(diagnostics) {
|
|
1777
|
-
const visible = diagnostics.slice(0, MAX_ESLINT_DIAGNOSTICS);
|
|
1778
|
-
const hidden = diagnostics.length - visible.length;
|
|
1779
|
-
const count = diagnostics.length;
|
|
1780
|
-
const noun = count === 1 ? "problem" : "problems";
|
|
1781
|
-
const lines = [` ${red("•")} [eslint] ${count} ESLint ${noun}`, ...visible.map((diagnostic) => `${diagnostic.file}:${diagnostic.line}:${diagnostic.column} ${diagnostic.severity} ${diagnostic.rule} — ${truncate(diagnostic.message, MAX_SUMMARY_LINE_LENGTH)}`)];
|
|
1782
|
-
if (hidden > 0) lines.push(dim(` +${hidden} more ESLint ${hidden === 1 ? "problem" : "problems"}. View full output for details.`));
|
|
1783
|
-
return lines.join("\n");
|
|
1784
|
-
}
|
|
1785
|
-
function firstMeaningfulLine(message) {
|
|
1786
|
-
return message.split("\n").map((l) => l.trim()).find((l) => l.length > 0 && !l.startsWith(">") && !l.startsWith("ELIFECYCLE")) ?? message;
|
|
1787
|
-
}
|
|
1788
|
-
function truncate(message, maxLength) {
|
|
1789
|
-
const collapsed = message.replace(/\s+/g, " ").trim();
|
|
1790
|
-
if (collapsed.length <= maxLength) return collapsed;
|
|
1791
|
-
return `${collapsed.slice(0, Math.max(0, maxLength - 1))}…`;
|
|
1792
|
-
}
|
|
1793
|
-
async function showCheckFailureMenu(errors, rawStderr, onRetry) {
|
|
1794
|
-
debug("showCheckFailureMenu: %d errors", errors.length);
|
|
1795
|
-
let clipboardCopied = false;
|
|
1796
|
-
p.note(formatCheckFailureSummary(errors), red("Pre-commit check failed"));
|
|
1797
|
-
while (true) {
|
|
1798
|
-
const choice = await p.select({
|
|
1799
|
-
message: "What do you want to do?",
|
|
1800
|
-
options: [
|
|
1801
|
-
{
|
|
1802
|
-
label: clipboardCopied ? `${green("✓")} Copy error report to clipboard` : "Copy error report to clipboard",
|
|
1803
|
-
value: "copy"
|
|
1804
|
-
},
|
|
1805
|
-
{
|
|
1806
|
-
label: "View full error output",
|
|
1807
|
-
value: "view",
|
|
1808
|
-
hint: "Show the raw stderr from checks"
|
|
1809
|
-
},
|
|
1810
|
-
{
|
|
1811
|
-
label: "Retry checks",
|
|
1812
|
-
value: "retry",
|
|
1813
|
-
hint: "Re-run checks after fixing errors"
|
|
1814
|
-
},
|
|
1815
|
-
{
|
|
1816
|
-
label: "Skip checks and commit",
|
|
1817
|
-
value: "skip"
|
|
1818
|
-
},
|
|
1819
|
-
{
|
|
1820
|
-
label: "Cancel",
|
|
1821
|
-
value: "cancel"
|
|
1822
|
-
}
|
|
1823
|
-
]
|
|
1824
|
-
});
|
|
1825
|
-
if (p.isCancel(choice)) {
|
|
1826
|
-
debug("showCheckFailureMenu: user cancelled");
|
|
1827
|
-
return "cancelled";
|
|
1828
|
-
}
|
|
1829
|
-
debug("showCheckFailureMenu: user chose %s", choice);
|
|
1830
|
-
switch (choice) {
|
|
1831
|
-
case "copy":
|
|
1832
|
-
if (await copyToClipboard(rawStderr)) {
|
|
1833
|
-
clipboardCopied = true;
|
|
1834
|
-
p.log.step(green("Copied to clipboard."));
|
|
1835
|
-
} else p.log.warn(red("No clipboard tool found. Install xclip, wl-copy, or xsel."));
|
|
1836
|
-
continue;
|
|
1837
|
-
case "view":
|
|
1838
|
-
p.note(rawStderr.trim() || "(no raw output)", "Full error output");
|
|
1839
|
-
continue;
|
|
1840
|
-
case "retry":
|
|
1841
|
-
if (onRetry) return "retried";
|
|
1842
|
-
return "retried";
|
|
1843
|
-
case "skip":
|
|
1844
|
-
p.log.info("Skipping checks and proceeding with commit...");
|
|
1845
|
-
return "skipped";
|
|
1846
|
-
case "cancel":
|
|
1847
|
-
p.outro(dim("Cancelled."));
|
|
1848
|
-
return "cancelled";
|
|
1849
|
-
}
|
|
1850
|
-
}
|
|
1851
|
-
}
|
|
1852
|
-
//#endregion
|
|
1853
|
-
//#region src/ui/check-summary.ts
|
|
1854
|
-
/**
|
|
1855
|
-
* Stop a check spinner with a per-tool summary of the check results.
|
|
1856
|
-
*
|
|
1857
|
-
* - On success: stops with "All checks passed" and prints a `✓ tool` line
|
|
1858
|
-
* for each result.
|
|
1859
|
-
* - On failure: stops with "N checks failed" (pluralized). Raw error output
|
|
1860
|
-
* is intentionally NOT printed here — callers handle failure display
|
|
1861
|
-
* (menu, raw print, etc.).
|
|
1862
|
-
*/
|
|
1863
|
-
function stopCheckSpinner(spinner, results) {
|
|
1864
|
-
if (results.ok) {
|
|
1865
|
-
spinner.stop("All checks passed");
|
|
1866
|
-
if (results.results.length > 0) log.info(results.results.map((r) => ` ${green("✓")} ${r.tool}`).join("\n"));
|
|
1867
|
-
} else {
|
|
1868
|
-
const failed = results.results.filter((r) => !r.ok);
|
|
1869
|
-
spinner.stop(`${failed.length} check${failed.length !== 1 ? "s" : ""} failed`);
|
|
1870
|
-
}
|
|
1871
|
-
}
|
|
1872
|
-
//#endregion
|
|
1873
|
-
//#region src/ui/grouping.ts
|
|
1874
|
-
async function showGroupingConfirmation(groups, excluded) {
|
|
1875
|
-
debug("showGroupingConfirmation: %d groups, %d excluded", groups.length, excluded.length);
|
|
1876
|
-
const lines = [];
|
|
1877
|
-
for (const group of groups) {
|
|
1878
|
-
lines.push(bold(group.name));
|
|
1879
|
-
lines.push(` ${dim(group.description)}`);
|
|
1880
|
-
lines.push(` ${green(String(group.files.length))} file${group.files.length !== 1 ? "s" : ""}`);
|
|
1881
|
-
for (const file of group.files) lines.push(` ${dim("•")} ${file}`);
|
|
1882
|
-
lines.push("");
|
|
1883
|
-
}
|
|
1884
|
-
if (excluded.length > 0) {
|
|
1885
|
-
lines.push(dim(`Excluded: ${excluded.length} file${excluded.length !== 1 ? "s" : ""}`));
|
|
1886
|
-
for (const file of excluded) lines.push(` ${dim("•")} ${dim(file)}`);
|
|
1887
|
-
}
|
|
1888
|
-
p.note(lines.join("\n"), "Proposed commit groups");
|
|
1889
|
-
const choice = await p.select({
|
|
1890
|
-
message: "Proceed with these groupings?",
|
|
1891
|
-
options: [{
|
|
1892
|
-
label: "Yes, commit all groups",
|
|
1893
|
-
value: "yes"
|
|
1894
|
-
}, {
|
|
1895
|
-
label: "No, cancel",
|
|
1896
|
-
value: "no"
|
|
1897
|
-
}]
|
|
1898
|
-
});
|
|
1899
|
-
if (p.isCancel(choice) || choice === "no") {
|
|
1900
|
-
debug("showGroupingConfirmation: user cancelled");
|
|
1901
|
-
return false;
|
|
1902
|
-
}
|
|
1903
|
-
debug("showGroupingConfirmation: user confirmed");
|
|
1904
|
-
return true;
|
|
1905
|
-
}
|
|
1906
|
-
function showGroupProgress(current, total, groupName) {
|
|
1907
|
-
p.log.info(`Commit group ${current} of ${total}: ${cyan(`"${groupName}"`)}`);
|
|
1908
|
-
}
|
|
1909
|
-
const statusLabel = (status) => {
|
|
1910
|
-
switch (status) {
|
|
1911
|
-
case "M": return yellow("M");
|
|
1912
|
-
case "A": return green("A");
|
|
1913
|
-
case "D": return red("D");
|
|
1914
|
-
case "?":
|
|
1915
|
-
case "??": return cyan("?");
|
|
1916
|
-
default: return dim(status);
|
|
1917
|
-
}
|
|
1918
|
-
};
|
|
1919
|
-
/** Display combined view: files with status indicators grouped by commit group */
|
|
1920
|
-
function showGroupedFiles(groups, changedFiles) {
|
|
1921
|
-
const statusMap = new Map(changedFiles.map((f) => [f.path, f.status]));
|
|
1922
|
-
const lines = [];
|
|
1923
|
-
for (let i = 0; i < groups.length; i++) {
|
|
1924
|
-
const group = groups[i];
|
|
1925
|
-
lines.push(`${bold(group.name)} ${dim("—")} ${group.files.length} file${group.files.length !== 1 ? "s" : ""}`);
|
|
1926
|
-
for (const file of group.files) {
|
|
1927
|
-
const status = statusMap.get(file) ?? "M";
|
|
1928
|
-
lines.push(` ${statusLabel(status)} ${file}`);
|
|
1929
|
-
}
|
|
1930
|
-
if (i < groups.length - 1) lines.push("");
|
|
1931
|
-
}
|
|
1932
|
-
p.note(lines.join("\n"), "Commit groups");
|
|
1758
|
+
exitCode = code;
|
|
1759
|
+
});
|
|
1760
|
+
child.stdin.write(content, (err) => {
|
|
1761
|
+
if (err) {
|
|
1762
|
+
done(false, "stdin write error");
|
|
1763
|
+
return;
|
|
1764
|
+
}
|
|
1765
|
+
child.stdin.end(() => {
|
|
1766
|
+
setTimeout(() => handleGracePeriod(settled, exitCode, stderrChunks, child, done), GRACE_PERIOD_MS);
|
|
1767
|
+
});
|
|
1768
|
+
});
|
|
1769
|
+
});
|
|
1933
1770
|
}
|
|
1934
1771
|
//#endregion
|
|
1935
1772
|
//#region src/ui/recovery-menu.ts
|
|
@@ -2109,6 +1946,259 @@ async function reviewCommitMessage(message, options) {
|
|
|
2109
1946
|
}
|
|
2110
1947
|
}
|
|
2111
1948
|
//#endregion
|
|
1949
|
+
//#region src/ui/check-failure-menu.ts
|
|
1950
|
+
const MAX_TSC_DIAGNOSTICS = 3;
|
|
1951
|
+
const MAX_ESLINT_DIAGNOSTICS = 3;
|
|
1952
|
+
const MAX_TEST_FAILURES = 3;
|
|
1953
|
+
const MAX_SUMMARY_LINE_LENGTH = 120;
|
|
1954
|
+
const TSC_DIAGNOSTIC = /^(.+?\.(?:ts|tsx|mts|cts|js|jsx|mjs|cjs))\((\d+),(\d+)\):\s+error\s+(TS\d+):\s+(.+)$/;
|
|
1955
|
+
const ESLINT_ERROR_LINE = /^\s*(\d+):(\d+)\s+(error|warning)\s+(.+)\s{2,}(\S+)\s*$/;
|
|
1956
|
+
const TEST_FILE_FAIL = /^\s*FAIL\s+(.+?\.(?:test|spec)\.[^\s>]+)\s*>\s*(.+)$/;
|
|
1957
|
+
function formatCheckFailureSummary(errors) {
|
|
1958
|
+
if (errors.length === 0) return "No check error details were parsed. View full output for details.";
|
|
1959
|
+
return errors.map((error) => formatCheckErrorSummary(error)).join("\n");
|
|
1960
|
+
}
|
|
1961
|
+
function formatCheckErrorSummary(error) {
|
|
1962
|
+
if (error.tool === "tsc") {
|
|
1963
|
+
const diagnostics = extractTscDiagnostics(error.raw || error.message);
|
|
1964
|
+
if (diagnostics.length > 0) return formatTscSummary(diagnostics);
|
|
1965
|
+
}
|
|
1966
|
+
if (error.tool === "eslint") {
|
|
1967
|
+
const diagnostics = extractEslintDiagnostics(error.raw || error.message);
|
|
1968
|
+
if (diagnostics.length > 0) return formatEslintSummary(diagnostics);
|
|
1969
|
+
}
|
|
1970
|
+
if (error.tool === "vitest" || error.tool === "jest") {
|
|
1971
|
+
const failures = extractTestFailures(error.raw || error.message);
|
|
1972
|
+
if (failures.length > 0) return formatTestFailureSummary(failures, error.tool);
|
|
1973
|
+
}
|
|
1974
|
+
const message = firstMeaningfulLine(error.message || error.raw);
|
|
1975
|
+
return ` ${red("•")} [${error.tool}] ${truncate(message, MAX_SUMMARY_LINE_LENGTH)}`;
|
|
1976
|
+
}
|
|
1977
|
+
function extractTscDiagnostics(raw) {
|
|
1978
|
+
return raw.split("\n").map((line) => line.trim()).map((line) => {
|
|
1979
|
+
const match = TSC_DIAGNOSTIC.exec(line);
|
|
1980
|
+
if (!match) return null;
|
|
1981
|
+
return {
|
|
1982
|
+
file: match[1] ?? "",
|
|
1983
|
+
line: match[2] ?? "",
|
|
1984
|
+
column: match[3] ?? "",
|
|
1985
|
+
code: match[4] ?? "",
|
|
1986
|
+
message: match[5] ?? ""
|
|
1987
|
+
};
|
|
1988
|
+
}).filter((diagnostic) => diagnostic !== null);
|
|
1989
|
+
}
|
|
1990
|
+
function formatTscSummary(diagnostics) {
|
|
1991
|
+
const visible = diagnostics.slice(0, MAX_TSC_DIAGNOSTICS);
|
|
1992
|
+
const hidden = diagnostics.length - visible.length;
|
|
1993
|
+
const lines = [` ${red("•")} [tsc] ${diagnostics.length} TypeScript error${diagnostics.length !== 1 ? "s" : ""}`, ...visible.map((diagnostic) => `${diagnostic.file}:${diagnostic.line}:${diagnostic.column} — error ${diagnostic.code}: ${truncate(diagnostic.message, MAX_SUMMARY_LINE_LENGTH)}`)];
|
|
1994
|
+
if (hidden > 0) lines.push(dim(` +${hidden} more TypeScript error${hidden !== 1 ? "s" : ""}. View full output for details.`));
|
|
1995
|
+
return lines.join("\n");
|
|
1996
|
+
}
|
|
1997
|
+
function extractEslintDiagnostics(raw) {
|
|
1998
|
+
const diagnostics = [];
|
|
1999
|
+
const lines = raw.split("\n");
|
|
2000
|
+
let currentFile = "";
|
|
2001
|
+
for (const line of lines) {
|
|
2002
|
+
if (!/^\s/.test(line) && line.includes("/") && !ESLINT_ERROR_LINE.test(line)) {
|
|
2003
|
+
currentFile = line.trim();
|
|
2004
|
+
continue;
|
|
2005
|
+
}
|
|
2006
|
+
const match = ESLINT_ERROR_LINE.exec(line);
|
|
2007
|
+
if (match) diagnostics.push({
|
|
2008
|
+
file: currentFile || "unknown",
|
|
2009
|
+
line: match[1] ?? "",
|
|
2010
|
+
column: match[2] ?? "",
|
|
2011
|
+
severity: match[3] ?? "",
|
|
2012
|
+
message: (match[4] ?? "").trim(),
|
|
2013
|
+
rule: match[5] ?? ""
|
|
2014
|
+
});
|
|
2015
|
+
}
|
|
2016
|
+
return diagnostics;
|
|
2017
|
+
}
|
|
2018
|
+
function extractTestFailures(raw) {
|
|
2019
|
+
const failures = [];
|
|
2020
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2021
|
+
for (const line of raw.split("\n")) {
|
|
2022
|
+
const match = TEST_FILE_FAIL.exec(line);
|
|
2023
|
+
if (!match) continue;
|
|
2024
|
+
const file = (match[1] ?? "").trim();
|
|
2025
|
+
const name = (match[2] ?? "").trim();
|
|
2026
|
+
if (!file || !name) continue;
|
|
2027
|
+
const key = `${file}\u0000${name}`;
|
|
2028
|
+
if (seen.has(key)) continue;
|
|
2029
|
+
seen.add(key);
|
|
2030
|
+
failures.push({
|
|
2031
|
+
file,
|
|
2032
|
+
name
|
|
2033
|
+
});
|
|
2034
|
+
}
|
|
2035
|
+
return failures;
|
|
2036
|
+
}
|
|
2037
|
+
function formatTestFailureSummary(failures, tool) {
|
|
2038
|
+
const total = failures.length;
|
|
2039
|
+
const visible = failures.slice(0, MAX_TEST_FAILURES);
|
|
2040
|
+
const hidden = total - visible.length;
|
|
2041
|
+
const fileCount = new Set(failures.map((f) => f.file)).size;
|
|
2042
|
+
const testNoun = total === 1 ? "test" : "tests";
|
|
2043
|
+
const fileNoun = fileCount === 1 ? "file" : "files";
|
|
2044
|
+
const lines = [` ${red("•")} [${tool}] ${total} failed ${testNoun} in ${fileCount} ${fileNoun}`];
|
|
2045
|
+
const byFile = /* @__PURE__ */ new Map();
|
|
2046
|
+
for (const failure of visible) {
|
|
2047
|
+
const names = byFile.get(failure.file) ?? [];
|
|
2048
|
+
names.push(failure.name);
|
|
2049
|
+
byFile.set(failure.file, names);
|
|
2050
|
+
}
|
|
2051
|
+
for (const [file, names] of byFile) {
|
|
2052
|
+
lines.push(` ${truncate(file, MAX_SUMMARY_LINE_LENGTH)}`);
|
|
2053
|
+
for (const name of names) lines.push(` ${red("×")} ${truncate(name, MAX_SUMMARY_LINE_LENGTH)}`);
|
|
2054
|
+
}
|
|
2055
|
+
if (hidden > 0) lines.push(dim(` +${hidden} more failed ${hidden === 1 ? "test" : "tests"}. View full output for details.`));
|
|
2056
|
+
return lines.join("\n");
|
|
2057
|
+
}
|
|
2058
|
+
function formatEslintSummary(diagnostics) {
|
|
2059
|
+
const visible = diagnostics.slice(0, MAX_ESLINT_DIAGNOSTICS);
|
|
2060
|
+
const hidden = diagnostics.length - visible.length;
|
|
2061
|
+
const count = diagnostics.length;
|
|
2062
|
+
const noun = count === 1 ? "problem" : "problems";
|
|
2063
|
+
const lines = [` ${red("•")} [eslint] ${count} ESLint ${noun}`, ...visible.map((diagnostic) => `${diagnostic.file}:${diagnostic.line}:${diagnostic.column} ${diagnostic.severity} ${diagnostic.rule} — ${truncate(diagnostic.message, MAX_SUMMARY_LINE_LENGTH)}`)];
|
|
2064
|
+
if (hidden > 0) lines.push(dim(` +${hidden} more ESLint ${hidden === 1 ? "problem" : "problems"}. View full output for details.`));
|
|
2065
|
+
return lines.join("\n");
|
|
2066
|
+
}
|
|
2067
|
+
function firstMeaningfulLine(message) {
|
|
2068
|
+
return message.split("\n").map((l) => l.trim()).find((l) => l.length > 0 && !l.startsWith(">") && !l.startsWith("ELIFECYCLE")) ?? message;
|
|
2069
|
+
}
|
|
2070
|
+
function truncate(message, maxLength) {
|
|
2071
|
+
const collapsed = message.replace(/\s+/g, " ").trim();
|
|
2072
|
+
if (collapsed.length <= maxLength) return collapsed;
|
|
2073
|
+
return `${collapsed.slice(0, Math.max(0, maxLength - 1))}…`;
|
|
2074
|
+
}
|
|
2075
|
+
async function showCheckFailureMenu(errors, rawStderr, onRetry) {
|
|
2076
|
+
debug("showCheckFailureMenu: %d errors", errors.length);
|
|
2077
|
+
let clipboardCopied = false;
|
|
2078
|
+
p.note(formatCheckFailureSummary(errors), red("Pre-commit check failed"));
|
|
2079
|
+
while (true) {
|
|
2080
|
+
const choice = await p.select({
|
|
2081
|
+
message: "What do you want to do?",
|
|
2082
|
+
options: [
|
|
2083
|
+
{
|
|
2084
|
+
label: clipboardCopied ? `${green("✓")} Copy error report to clipboard` : "Copy error report to clipboard",
|
|
2085
|
+
value: "copy"
|
|
2086
|
+
},
|
|
2087
|
+
{
|
|
2088
|
+
label: "View full error output",
|
|
2089
|
+
value: "view",
|
|
2090
|
+
hint: "Show the raw stderr from checks"
|
|
2091
|
+
},
|
|
2092
|
+
{
|
|
2093
|
+
label: "Retry checks",
|
|
2094
|
+
value: "retry",
|
|
2095
|
+
hint: "Re-run checks after fixing errors"
|
|
2096
|
+
},
|
|
2097
|
+
{
|
|
2098
|
+
label: "Skip checks and commit",
|
|
2099
|
+
value: "skip"
|
|
2100
|
+
},
|
|
2101
|
+
{
|
|
2102
|
+
label: "Cancel",
|
|
2103
|
+
value: "cancel"
|
|
2104
|
+
}
|
|
2105
|
+
]
|
|
2106
|
+
});
|
|
2107
|
+
if (p.isCancel(choice)) {
|
|
2108
|
+
debug("showCheckFailureMenu: user cancelled");
|
|
2109
|
+
return "cancelled";
|
|
2110
|
+
}
|
|
2111
|
+
debug("showCheckFailureMenu: user chose %s", choice);
|
|
2112
|
+
switch (choice) {
|
|
2113
|
+
case "copy":
|
|
2114
|
+
if (await copyToClipboard(rawStderr)) {
|
|
2115
|
+
clipboardCopied = true;
|
|
2116
|
+
p.log.step(green("Copied to clipboard."));
|
|
2117
|
+
} else p.log.warn(red("No clipboard tool found. Install xclip, wl-copy, or xsel."));
|
|
2118
|
+
continue;
|
|
2119
|
+
case "view":
|
|
2120
|
+
p.note(rawStderr.trim() || "(no raw output)", "Full error output");
|
|
2121
|
+
continue;
|
|
2122
|
+
case "retry":
|
|
2123
|
+
if (onRetry) return "retried";
|
|
2124
|
+
return "retried";
|
|
2125
|
+
case "skip":
|
|
2126
|
+
p.log.info("Skipping checks and proceeding with commit...");
|
|
2127
|
+
return "skipped";
|
|
2128
|
+
case "cancel":
|
|
2129
|
+
p.outro(dim("Cancelled."));
|
|
2130
|
+
return "cancelled";
|
|
2131
|
+
}
|
|
2132
|
+
}
|
|
2133
|
+
}
|
|
2134
|
+
//#endregion
|
|
2135
|
+
//#region src/ui/check-summary.ts
|
|
2136
|
+
/**
|
|
2137
|
+
* Stop a check spinner with a per-tool summary of the check results.
|
|
2138
|
+
*
|
|
2139
|
+
* - On success: stops with "All checks passed" and prints a `✓ tool` line
|
|
2140
|
+
* for each result.
|
|
2141
|
+
* - On failure: stops with "N checks failed" (pluralized). Raw error output
|
|
2142
|
+
* is intentionally NOT printed here — callers handle failure display
|
|
2143
|
+
* (menu, raw print, etc.).
|
|
2144
|
+
*/
|
|
2145
|
+
function stopCheckSpinner(spinner, results) {
|
|
2146
|
+
if (results.ok) {
|
|
2147
|
+
spinner.stop("All checks passed");
|
|
2148
|
+
if (results.results.length > 0) log.info(results.results.map((r) => ` ${green("✓")} ${r.tool}`).join("\n"));
|
|
2149
|
+
} else {
|
|
2150
|
+
const failed = results.results.filter((r) => !r.ok);
|
|
2151
|
+
spinner.stop(`${failed.length} check${failed.length !== 1 ? "s" : ""} failed`);
|
|
2152
|
+
}
|
|
2153
|
+
}
|
|
2154
|
+
//#endregion
|
|
2155
|
+
//#region src/commands/check-phase.ts
|
|
2156
|
+
/**
|
|
2157
|
+
* Run user-defined pre-commit checks with an interactive failure menu.
|
|
2158
|
+
*
|
|
2159
|
+
* Single entry point for the check-execution pipeline shared by `runPreCommitChecks`
|
|
2160
|
+
* (post-staging, normal commit flow) and `runAutoGroupFlow` (pre-staging, auto-group
|
|
2161
|
+
* flow). Encapsulates: detectConfig guard → spinner → runAllChecks → retry loop with
|
|
2162
|
+
* `showCheckFailureMenu`.
|
|
2163
|
+
*
|
|
2164
|
+
* Caller responsibilities:
|
|
2165
|
+
* - Skip when `noCheck` is set (caller's policy).
|
|
2166
|
+
* - Skip when there are no files to check (caller has the file list context).
|
|
2167
|
+
* - Derive `files` in **repo-root-relative** form so they match `.cmintrc` globs.
|
|
2168
|
+
* Post-staging callers should use `getStagedFiles()`; pre-staging callers should
|
|
2169
|
+
* use `resolveToRepoRoot()` since the index doesn't yet contain those paths.
|
|
2170
|
+
*
|
|
2171
|
+
* Returns the outcome so the caller can decide how to handle cancellation
|
|
2172
|
+
* (`process.exit(1)` in the commit flow, `return "cancelled"` in auto-group).
|
|
2173
|
+
*/
|
|
2174
|
+
async function runCheckPhaseInteractive(repoRoot, files, timeout, onRetry) {
|
|
2175
|
+
if (!await detectConfig(repoRoot)) return "passed";
|
|
2176
|
+
debug("Running user checks on %d files...", files.length);
|
|
2177
|
+
const ck = spinner();
|
|
2178
|
+
ck.start("Running checks...");
|
|
2179
|
+
let checkResults = await runAllChecks(repoRoot, files, timeout);
|
|
2180
|
+
stopCheckSpinner(ck, checkResults);
|
|
2181
|
+
debug("Check results: ok=%s, count=%d", checkResults.ok, checkResults.results.length);
|
|
2182
|
+
while (!checkResults.ok) {
|
|
2183
|
+
const rawOutput = checkResults.results.filter((r) => !r.ok).map((r) => `[${r.tool}]\n${r.stdout}\n${r.stderr}`.trim()).join("\n\n");
|
|
2184
|
+
const menuResult = await showCheckFailureMenu(parseCheckErrors(rawOutput), rawOutput, async () => {
|
|
2185
|
+
return (await runAllChecks(repoRoot, files, timeout)).ok;
|
|
2186
|
+
});
|
|
2187
|
+
if (menuResult === "cancelled") return "cancelled";
|
|
2188
|
+
if (menuResult === "retried") {
|
|
2189
|
+
debug("Re-running checks after retry...");
|
|
2190
|
+
if (onRetry) await onRetry();
|
|
2191
|
+
ck.start("Running checks...");
|
|
2192
|
+
checkResults = await runAllChecks(repoRoot, files, timeout);
|
|
2193
|
+
stopCheckSpinner(ck, checkResults);
|
|
2194
|
+
debug("Retry check results: ok=%s, count=%d", checkResults.ok, checkResults.results.length);
|
|
2195
|
+
continue;
|
|
2196
|
+
}
|
|
2197
|
+
break;
|
|
2198
|
+
}
|
|
2199
|
+
return checkResults.ok ? "passed" : "skipped";
|
|
2200
|
+
}
|
|
2201
|
+
//#endregion
|
|
2112
2202
|
//#region src/commands/auto-group.ts
|
|
2113
2203
|
async function runAutoGroupFlow(changedFiles, flags) {
|
|
2114
2204
|
const { included, excluded } = filterExcludedFiles(changedFiles);
|
|
@@ -2137,33 +2227,7 @@ async function runAutoGroupFlow(changedFiles, flags) {
|
|
|
2137
2227
|
}
|
|
2138
2228
|
if (!flags.noCheck) {
|
|
2139
2229
|
const { getRepoRoot } = await Promise.resolve().then(() => git_exports);
|
|
2140
|
-
|
|
2141
|
-
const allFiles = included.filter((f) => f.status !== "D").map((f) => f.path);
|
|
2142
|
-
if (await detectConfig(repoRoot)) {
|
|
2143
|
-
debug("Running user checks on %d files...", allFiles.length);
|
|
2144
|
-
const ck = spinner();
|
|
2145
|
-
ck.start("Running checks...");
|
|
2146
|
-
let checkResults = await runAllChecks(repoRoot, allFiles, 6e4);
|
|
2147
|
-
debug("Check results: ok=%s, count=%d", checkResults.ok, checkResults.results.length);
|
|
2148
|
-
while (!checkResults.ok) {
|
|
2149
|
-
const failed = checkResults.results.filter((r) => !r.ok);
|
|
2150
|
-
ck.stop(`${failed.length} check(s) failed`);
|
|
2151
|
-
const rawOutput = failed.map((r) => `[${r.tool}]\n${r.stdout}\n${r.stderr}`.trim()).join("\n\n");
|
|
2152
|
-
const menuResult = await showCheckFailureMenu(parseCheckErrors(rawOutput), rawOutput, async () => {
|
|
2153
|
-
return (await runAllChecks(repoRoot, allFiles, 6e4)).ok;
|
|
2154
|
-
});
|
|
2155
|
-
if (menuResult === "cancelled") return "cancelled";
|
|
2156
|
-
if (menuResult === "retried") {
|
|
2157
|
-
debug("Re-running checks after retry...");
|
|
2158
|
-
ck.start("Running checks...");
|
|
2159
|
-
checkResults = await runAllChecks(repoRoot, allFiles, 6e4);
|
|
2160
|
-
debug("Retry check results: ok=%s, count=%d", checkResults.ok, checkResults.results.length);
|
|
2161
|
-
continue;
|
|
2162
|
-
}
|
|
2163
|
-
break;
|
|
2164
|
-
}
|
|
2165
|
-
if (checkResults.ok) stopCheckSpinner(ck, checkResults);
|
|
2166
|
-
}
|
|
2230
|
+
if (await runCheckPhaseInteractive(await getRepoRoot(), await resolveToRepoRoot(included.filter((f) => f.status !== "D").map((f) => f.path)), 6e4) === "cancelled") return "cancelled";
|
|
2167
2231
|
}
|
|
2168
2232
|
const config = await readConfig();
|
|
2169
2233
|
const resolvedProvider = config.provider ?? "groq";
|
|
@@ -2401,7 +2465,7 @@ async function agentCommand(flags) {
|
|
|
2401
2465
|
const repoRoot = await getRepoRoot();
|
|
2402
2466
|
if (await detectConfig(repoRoot)) {
|
|
2403
2467
|
debug("Running user checks on changed files...");
|
|
2404
|
-
const checkResults = await runAllChecks(repoRoot, changedFiles.filter((f) => f.status !== "D").map((f) => f.path), 6e4);
|
|
2468
|
+
const checkResults = await runAllChecks(repoRoot, await resolveToRepoRoot(changedFiles.filter((f) => f.status !== "D").map((f) => f.path)), 6e4);
|
|
2405
2469
|
if (!checkResults.ok) {
|
|
2406
2470
|
const errorMessages = checkResults.results.filter((r) => !r.ok).map((r) => `[${r.tool}]\n${r.stdout}\n${r.stderr}`.trim()).filter(Boolean);
|
|
2407
2471
|
const parsed = parseCheckErrors(errorMessages.join("\n\n"));
|
|
@@ -3206,7 +3270,7 @@ async function handleStaging(changedFiles, flags) {
|
|
|
3206
3270
|
}
|
|
3207
3271
|
if (stagingResult === "checks") {
|
|
3208
3272
|
await stageAll();
|
|
3209
|
-
const allFiles =
|
|
3273
|
+
const allFiles = await getStagedFiles();
|
|
3210
3274
|
if (await detectConfig(repoRoot)) {
|
|
3211
3275
|
const ckSpinner = spinner();
|
|
3212
3276
|
ckSpinner.start("Running checks...");
|
|
@@ -3245,33 +3309,13 @@ async function handleStaging(changedFiles, flags) {
|
|
|
3245
3309
|
async function runPreCommitChecks(changedFiles, noCheck) {
|
|
3246
3310
|
if (noCheck) return;
|
|
3247
3311
|
const checkRoot = await getRepoRoot();
|
|
3248
|
-
const stagedFileList =
|
|
3312
|
+
const stagedFileList = await getStagedFiles();
|
|
3249
3313
|
if (stagedFileList.length === 0) return;
|
|
3250
|
-
|
|
3251
|
-
|
|
3252
|
-
|
|
3253
|
-
|
|
3254
|
-
|
|
3255
|
-
debug("Check results: ok=%s, count=%d", checkResults.ok, checkResults.results.length);
|
|
3256
|
-
while (!checkResults.ok) {
|
|
3257
|
-
const rawOutput = checkResults.results.filter((r) => !r.ok).map((r) => `[${r.tool}]\n${r.stdout}\n${r.stderr}`.trim()).join("\n\n");
|
|
3258
|
-
const menuResult = await showCheckFailureMenu(parseCheckErrors(rawOutput), rawOutput, async () => {
|
|
3259
|
-
return (await runAllChecks(checkRoot, stagedFileList, 6e4)).ok;
|
|
3260
|
-
});
|
|
3261
|
-
if (menuResult === "cancelled") process.exit(1);
|
|
3262
|
-
if (menuResult === "retried") {
|
|
3263
|
-
debug("Re-staging files and re-running checks after retry...");
|
|
3264
|
-
await stageAll();
|
|
3265
|
-
const ckSpinner = spinner();
|
|
3266
|
-
ckSpinner.start("Running checks...");
|
|
3267
|
-
checkResults = await runAllChecks(checkRoot, stagedFileList, 6e4);
|
|
3268
|
-
debug("Retry check results: ok=%s, count=%d", checkResults.ok, checkResults.results.length);
|
|
3269
|
-
stopCheckSpinner(ckSpinner, checkResults);
|
|
3270
|
-
continue;
|
|
3271
|
-
}
|
|
3272
|
-
break;
|
|
3273
|
-
}
|
|
3274
|
-
await restageFormatterModifications(stagedFileList);
|
|
3314
|
+
if (await runCheckPhaseInteractive(checkRoot, stagedFileList, 6e4, async () => {
|
|
3315
|
+
debug("Re-staging files before retry...");
|
|
3316
|
+
await stageAll();
|
|
3317
|
+
}) === "cancelled") process.exit(1);
|
|
3318
|
+
await restageFormatterModifications(changedFiles.filter((f) => f.staged && f.status !== "D").map((f) => f.path));
|
|
3275
3319
|
}
|
|
3276
3320
|
/**
|
|
3277
3321
|
* Re-stage staged files whose working-tree content diverged from the index after checks ran.
|