@kyubiware/commit-mint 0.8.0 → 0.8.2
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.d.mts +6 -1
- package/dist/cli.d.mts.map +1 -0
- package/dist/cli.mjs +1193 -406
- package/dist/cli.mjs.map +1 -1
- package/package.json +1 -1
package/dist/cli.mjs
CHANGED
|
@@ -10,12 +10,16 @@ import { execa } from "execa";
|
|
|
10
10
|
import picomatch from "picomatch";
|
|
11
11
|
import ini from "ini";
|
|
12
12
|
import { createHash } from "node:crypto";
|
|
13
|
-
import * as p from "@clack/prompts";
|
|
14
|
-
import { intro, isCancel, log, outro, spinner } from "@clack/prompts";
|
|
13
|
+
import * as p$1 from "@clack/prompts";
|
|
14
|
+
import { S_BAR, S_BAR_END, S_RADIO_ACTIVE, S_RADIO_INACTIVE, intro, isCancel, limitOptions, log, outro, spinner, symbol } from "@clack/prompts";
|
|
15
15
|
import { spawn } from "node:child_process";
|
|
16
16
|
import semver from "semver";
|
|
17
|
+
import { styleText } from "node:util";
|
|
18
|
+
import { stdin, stdout } from "node:process";
|
|
19
|
+
import E from "node:readline";
|
|
17
20
|
//#region \0rolldown/runtime.js
|
|
18
21
|
var __defProp = Object.defineProperty;
|
|
22
|
+
var __commonJSMin = (cb, mod) => () => (mod || (cb((mod = { exports: {} }).exports, mod), cb = null), mod.exports);
|
|
19
23
|
var __exportAll = (all, no_symbols) => {
|
|
20
24
|
let target = {};
|
|
21
25
|
for (var name in all) __defProp(target, name, {
|
|
@@ -29,7 +33,7 @@ var __exportAll = (all, no_symbols) => {
|
|
|
29
33
|
//#region package.json
|
|
30
34
|
var package_default = {
|
|
31
35
|
name: "@kyubiware/commit-mint",
|
|
32
|
-
version: "0.8.
|
|
36
|
+
version: "0.8.2",
|
|
33
37
|
description: "🌿 AI-powered git commit tool — auto-group changed files, generate messages, run pre-commit checks",
|
|
34
38
|
type: "module",
|
|
35
39
|
bin: { "cmint": "./dist/cli.mjs" },
|
|
@@ -673,7 +677,10 @@ async function runCommand(command, timeout, repoRoot) {
|
|
|
673
677
|
timeout,
|
|
674
678
|
all: true,
|
|
675
679
|
preferLocal: true,
|
|
676
|
-
...repoRoot ? {
|
|
680
|
+
...repoRoot ? {
|
|
681
|
+
localDir: repoRoot,
|
|
682
|
+
cwd: repoRoot
|
|
683
|
+
} : {}
|
|
677
684
|
});
|
|
678
685
|
const ok = !result.failed;
|
|
679
686
|
debug("runCommand: %s — ok=%s", tool, ok);
|
|
@@ -924,8 +931,10 @@ var git_exports = /* @__PURE__ */ __exportAll({
|
|
|
924
931
|
getHead: () => getHead,
|
|
925
932
|
getRepoRoot: () => getRepoRoot,
|
|
926
933
|
getStagedDiff: () => getStagedDiff,
|
|
934
|
+
getStagedFiles: () => getStagedFiles,
|
|
927
935
|
getStatusShort: () => getStatusShort,
|
|
928
936
|
resetStaging: () => resetStaging,
|
|
937
|
+
resolveToRepoRoot: () => resolveToRepoRoot,
|
|
929
938
|
stageAll: () => stageAll,
|
|
930
939
|
stageFiles: () => stageFiles
|
|
931
940
|
});
|
|
@@ -1043,6 +1052,44 @@ async function getChangedFiles() {
|
|
|
1043
1052
|
debug("getChangedFiles:", files.length, "files");
|
|
1044
1053
|
return files;
|
|
1045
1054
|
}
|
|
1055
|
+
/**
|
|
1056
|
+
* Return staged file paths relative to the repository root, excluding deletions.
|
|
1057
|
+
*
|
|
1058
|
+
* `git status --short` reports paths relative to the current working directory,
|
|
1059
|
+
* but `.cmintrc` globs are written from the repo root (matching lint-staged
|
|
1060
|
+
* conventions). Use this helper whenever staged paths need to match repo-root
|
|
1061
|
+
* globs. `--diff-filter=d` excludes staged deletions so check commands don't
|
|
1062
|
+
* receive paths whose content no longer exists.
|
|
1063
|
+
*/
|
|
1064
|
+
async function getStagedFiles() {
|
|
1065
|
+
const { stdout } = await execa("git", [
|
|
1066
|
+
"diff",
|
|
1067
|
+
"--cached",
|
|
1068
|
+
"--name-only",
|
|
1069
|
+
"--diff-filter=d"
|
|
1070
|
+
]);
|
|
1071
|
+
const files = stdout.split("\n").map((line) => line.trim()).filter(Boolean);
|
|
1072
|
+
debug("getStagedFiles:", files.length, "files");
|
|
1073
|
+
return files;
|
|
1074
|
+
}
|
|
1075
|
+
/**
|
|
1076
|
+
* Convert cwd-relative file paths to repo-root-relative paths.
|
|
1077
|
+
*
|
|
1078
|
+
* Uses `git rev-parse --show-prefix` to discover the prefix of the current
|
|
1079
|
+
* working directory relative to the repo root (e.g. `"extension/"` when cwd
|
|
1080
|
+
* is `<repo>/extension`, or `""` when at the repo root). Useful when a caller
|
|
1081
|
+
* has cwd-relative paths from `getChangedFiles()` but needs to match them
|
|
1082
|
+
* against repo-root-relative `.cmintrc` globs (e.g. the auto-group flow,
|
|
1083
|
+
* which runs checks BEFORE files are staged — so `getStagedFiles()` can't be
|
|
1084
|
+
* used because the index doesn't yet contain those paths).
|
|
1085
|
+
*/
|
|
1086
|
+
async function resolveToRepoRoot(cwdRelativePaths) {
|
|
1087
|
+
if (cwdRelativePaths.length === 0) return [];
|
|
1088
|
+
const { stdout } = await execa("git", ["rev-parse", "--show-prefix"]);
|
|
1089
|
+
const prefix = stdout.trim();
|
|
1090
|
+
if (!prefix) return [...cwdRelativePaths];
|
|
1091
|
+
return cwdRelativePaths.map((p) => `${prefix}${p}`);
|
|
1092
|
+
}
|
|
1046
1093
|
async function stageFiles(paths) {
|
|
1047
1094
|
debug("stageFiles:", paths);
|
|
1048
1095
|
await execa("git", ["add", ...paths]);
|
|
@@ -1586,6 +1633,97 @@ async function loadCachedCommit(repoPath) {
|
|
|
1586
1633
|
}
|
|
1587
1634
|
}
|
|
1588
1635
|
//#endregion
|
|
1636
|
+
//#region src/services/auto-accept.ts
|
|
1637
|
+
/** Parse a stored `auto-accept` INI value into a boolean.
|
|
1638
|
+
* Accepts true variants ("true", "1", "yes" — case-insensitive).
|
|
1639
|
+
* Handles boolean values from ini.parse (which converts unquoted
|
|
1640
|
+
* `true`/`false` to actual booleans).
|
|
1641
|
+
* Everything else (including undefined) returns false. */
|
|
1642
|
+
function parseAutoAcceptValue(value) {
|
|
1643
|
+
if (typeof value === "boolean") return value;
|
|
1644
|
+
if (typeof value !== "string" || !value) return false;
|
|
1645
|
+
return [
|
|
1646
|
+
"true",
|
|
1647
|
+
"1",
|
|
1648
|
+
"yes"
|
|
1649
|
+
].includes(value.toLowerCase());
|
|
1650
|
+
}
|
|
1651
|
+
/** Read the persisted auto-accept preference from `~/.commit-mint`. */
|
|
1652
|
+
async function getAutoAccept() {
|
|
1653
|
+
const raw = (await readConfig())["auto-accept"];
|
|
1654
|
+
const enabled = parseAutoAcceptValue(raw);
|
|
1655
|
+
debug("getAutoAccept: raw=%s enabled=%s", raw, enabled);
|
|
1656
|
+
return enabled;
|
|
1657
|
+
}
|
|
1658
|
+
/** Persist the auto-accept preference to `~/.commit-mint`. */
|
|
1659
|
+
async function setAutoAccept(enabled) {
|
|
1660
|
+
const value = enabled ? "true" : "false";
|
|
1661
|
+
debug("setAutoAccept: %s", value);
|
|
1662
|
+
await writeConfig({ "auto-accept": value });
|
|
1663
|
+
}
|
|
1664
|
+
//#endregion
|
|
1665
|
+
//#region src/ui/grouping.ts
|
|
1666
|
+
async function showGroupingConfirmation(groups, excluded) {
|
|
1667
|
+
debug("showGroupingConfirmation: %d groups, %d excluded", groups.length, excluded.length);
|
|
1668
|
+
const lines = [];
|
|
1669
|
+
for (const group of groups) {
|
|
1670
|
+
lines.push(bold(group.name));
|
|
1671
|
+
lines.push(` ${dim(group.description)}`);
|
|
1672
|
+
lines.push(` ${green(String(group.files.length))} file${group.files.length !== 1 ? "s" : ""}`);
|
|
1673
|
+
for (const file of group.files) lines.push(` ${dim("•")} ${file}`);
|
|
1674
|
+
lines.push("");
|
|
1675
|
+
}
|
|
1676
|
+
if (excluded.length > 0) {
|
|
1677
|
+
lines.push(dim(`Excluded: ${excluded.length} file${excluded.length !== 1 ? "s" : ""}`));
|
|
1678
|
+
for (const file of excluded) lines.push(` ${dim("•")} ${dim(file)}`);
|
|
1679
|
+
}
|
|
1680
|
+
p$1.note(lines.join("\n"), "Proposed commit groups");
|
|
1681
|
+
const choice = await p$1.select({
|
|
1682
|
+
message: "Proceed with these groupings?",
|
|
1683
|
+
options: [{
|
|
1684
|
+
label: "Yes, commit all groups",
|
|
1685
|
+
value: "yes"
|
|
1686
|
+
}, {
|
|
1687
|
+
label: "No, cancel",
|
|
1688
|
+
value: "no"
|
|
1689
|
+
}]
|
|
1690
|
+
});
|
|
1691
|
+
if (p$1.isCancel(choice) || choice === "no") {
|
|
1692
|
+
debug("showGroupingConfirmation: user cancelled");
|
|
1693
|
+
return false;
|
|
1694
|
+
}
|
|
1695
|
+
debug("showGroupingConfirmation: user confirmed");
|
|
1696
|
+
return true;
|
|
1697
|
+
}
|
|
1698
|
+
function showGroupProgress(current, total, groupName) {
|
|
1699
|
+
p$1.log.info(`Commit group ${current} of ${total}: ${cyan(`"${groupName}"`)}`);
|
|
1700
|
+
}
|
|
1701
|
+
const statusLabel = (status) => {
|
|
1702
|
+
switch (status) {
|
|
1703
|
+
case "M": return yellow("M");
|
|
1704
|
+
case "A": return green("A");
|
|
1705
|
+
case "D": return red("D");
|
|
1706
|
+
case "?":
|
|
1707
|
+
case "??": return cyan("?");
|
|
1708
|
+
default: return dim(status);
|
|
1709
|
+
}
|
|
1710
|
+
};
|
|
1711
|
+
/** Display combined view: files with status indicators grouped by commit group */
|
|
1712
|
+
function showGroupedFiles(groups, changedFiles) {
|
|
1713
|
+
const statusMap = new Map(changedFiles.map((f) => [f.path, f.status]));
|
|
1714
|
+
const lines = [];
|
|
1715
|
+
for (let i = 0; i < groups.length; i++) {
|
|
1716
|
+
const group = groups[i];
|
|
1717
|
+
lines.push(`${bold(group.name)} ${dim("—")} ${group.files.length} file${group.files.length !== 1 ? "s" : ""}`);
|
|
1718
|
+
for (const file of group.files) {
|
|
1719
|
+
const status = statusMap.get(file) ?? "M";
|
|
1720
|
+
lines.push(` ${statusLabel(status)} ${file}`);
|
|
1721
|
+
}
|
|
1722
|
+
if (i < groups.length - 1) lines.push("");
|
|
1723
|
+
}
|
|
1724
|
+
p$1.note(lines.join("\n"), "Commit groups");
|
|
1725
|
+
}
|
|
1726
|
+
//#endregion
|
|
1589
1727
|
//#region src/services/clipboard.ts
|
|
1590
1728
|
/** Milliseconds to wait after stdin closes for quick exit failures. */
|
|
1591
1729
|
const GRACE_PERIOD_MS = 150;
|
|
@@ -1664,6 +1802,183 @@ function tryCopy(cmd, args, content) {
|
|
|
1664
1802
|
});
|
|
1665
1803
|
}
|
|
1666
1804
|
//#endregion
|
|
1805
|
+
//#region src/ui/recovery-menu.ts
|
|
1806
|
+
async function showRecoveryMenu(errors, onRetry, onSkipHooks, onRestage, message, rawStderr) {
|
|
1807
|
+
debug("showRecoveryMenu: %d errors", errors.length);
|
|
1808
|
+
let clipboardCopied = false;
|
|
1809
|
+
let showNote = true;
|
|
1810
|
+
while (true) {
|
|
1811
|
+
if (showNote) {
|
|
1812
|
+
p$1.note(errors.map((e) => ` ${red("•")} [${e.tool}] ${e.message}`).join("\n"), red(bold("Pre-commit hook failed")));
|
|
1813
|
+
showNote = false;
|
|
1814
|
+
}
|
|
1815
|
+
const choice = await p$1.select({
|
|
1816
|
+
message: "What do you want to do?",
|
|
1817
|
+
options: [
|
|
1818
|
+
{
|
|
1819
|
+
label: clipboardCopied ? `${green("✓")} Copy error report to clipboard` : "Copy error report to clipboard",
|
|
1820
|
+
value: "clipboard",
|
|
1821
|
+
hint: clipboardCopied ? "Copied!" : "Paste into another terminal for an AI agent"
|
|
1822
|
+
},
|
|
1823
|
+
{
|
|
1824
|
+
label: "View full error output",
|
|
1825
|
+
value: "view",
|
|
1826
|
+
hint: "Show the raw stderr from hooks"
|
|
1827
|
+
},
|
|
1828
|
+
{
|
|
1829
|
+
label: "Skip hooks and commit (--no-verify)",
|
|
1830
|
+
value: "skip",
|
|
1831
|
+
hint: "Commit anyway, fix later"
|
|
1832
|
+
},
|
|
1833
|
+
{
|
|
1834
|
+
label: "Re-stage files and retry",
|
|
1835
|
+
value: "restage",
|
|
1836
|
+
hint: "Pick up fixes from another terminal"
|
|
1837
|
+
},
|
|
1838
|
+
{
|
|
1839
|
+
label: "Edit commit message",
|
|
1840
|
+
value: "edit",
|
|
1841
|
+
hint: "Modify the message before retrying"
|
|
1842
|
+
},
|
|
1843
|
+
{
|
|
1844
|
+
label: "Cancel",
|
|
1845
|
+
value: "cancel"
|
|
1846
|
+
}
|
|
1847
|
+
]
|
|
1848
|
+
});
|
|
1849
|
+
if (p$1.isCancel(choice)) {
|
|
1850
|
+
debug("showRecoveryMenu: user cancelled");
|
|
1851
|
+
p$1.outro(yellow("Cancelled. Message cached for --retry."));
|
|
1852
|
+
return "cancelled";
|
|
1853
|
+
}
|
|
1854
|
+
debug("showRecoveryMenu: user chose %s", choice);
|
|
1855
|
+
switch (choice) {
|
|
1856
|
+
case "clipboard":
|
|
1857
|
+
if (await copyToClipboard(rawStderr)) {
|
|
1858
|
+
clipboardCopied = true;
|
|
1859
|
+
p$1.log.step(green("Copied to clipboard."));
|
|
1860
|
+
} else p$1.log.warn(red("No clipboard tool found. Install xclip, wl-copy, or xsel."));
|
|
1861
|
+
continue;
|
|
1862
|
+
case "view":
|
|
1863
|
+
p$1.note(rawStderr.trim() || "(no raw output)", "Full error output");
|
|
1864
|
+
showNote = true;
|
|
1865
|
+
continue;
|
|
1866
|
+
case "skip":
|
|
1867
|
+
p$1.log.info(yellow("Committing with --no-verify..."));
|
|
1868
|
+
if (await onSkipHooks(message)) {
|
|
1869
|
+
p$1.outro(green("Committed (hooks skipped)."));
|
|
1870
|
+
return "committed";
|
|
1871
|
+
} else {
|
|
1872
|
+
p$1.outro(red("Commit failed even with --no-verify."));
|
|
1873
|
+
return "failed";
|
|
1874
|
+
}
|
|
1875
|
+
case "restage":
|
|
1876
|
+
p$1.log.info(cyan("Re-staging and retrying..."));
|
|
1877
|
+
if (await onRestage()) {
|
|
1878
|
+
p$1.outro(green("Committed successfully."));
|
|
1879
|
+
return "committed";
|
|
1880
|
+
}
|
|
1881
|
+
showNote = true;
|
|
1882
|
+
continue;
|
|
1883
|
+
case "edit": {
|
|
1884
|
+
const edited = await p$1.text({
|
|
1885
|
+
message: "Edit commit message:",
|
|
1886
|
+
initialValue: message,
|
|
1887
|
+
validate: (v) => v?.trim() ? void 0 : "Message cannot be empty"
|
|
1888
|
+
});
|
|
1889
|
+
if (p$1.isCancel(edited)) {
|
|
1890
|
+
p$1.outro(yellow("Cancelled. Message cached for --retry."));
|
|
1891
|
+
return "cancelled";
|
|
1892
|
+
}
|
|
1893
|
+
if (await onRetry()) {
|
|
1894
|
+
p$1.outro(green("Committed successfully."));
|
|
1895
|
+
return "committed";
|
|
1896
|
+
} else {
|
|
1897
|
+
p$1.outro(red("Commit failed again."));
|
|
1898
|
+
return "failed";
|
|
1899
|
+
}
|
|
1900
|
+
}
|
|
1901
|
+
case "cancel":
|
|
1902
|
+
p$1.outro(dim("Message cached for --retry."));
|
|
1903
|
+
return "cancelled";
|
|
1904
|
+
}
|
|
1905
|
+
}
|
|
1906
|
+
}
|
|
1907
|
+
//#endregion
|
|
1908
|
+
//#region src/ui/review-message.ts
|
|
1909
|
+
async function handleEdit(message) {
|
|
1910
|
+
const { text } = await import("@clack/prompts");
|
|
1911
|
+
const edited = await text({
|
|
1912
|
+
message: "Edit commit message:",
|
|
1913
|
+
initialValue: message,
|
|
1914
|
+
validate: (v) => v?.trim() ? void 0 : "Message cannot be empty"
|
|
1915
|
+
});
|
|
1916
|
+
if (isCancel(edited)) {
|
|
1917
|
+
debug("User cancelled edit, returning to review menu");
|
|
1918
|
+
return null;
|
|
1919
|
+
}
|
|
1920
|
+
const newMessage = String(edited).trim();
|
|
1921
|
+
debug("Edited message:", newMessage);
|
|
1922
|
+
return newMessage;
|
|
1923
|
+
}
|
|
1924
|
+
async function handleRegenerate(regenerate) {
|
|
1925
|
+
const { log, text } = await import("@clack/prompts");
|
|
1926
|
+
const hint = await text({
|
|
1927
|
+
message: "Describe what this commit is about to guide regeneration:",
|
|
1928
|
+
validate: (v) => v?.trim() ? void 0 : "Hint cannot be empty"
|
|
1929
|
+
});
|
|
1930
|
+
if (isCancel(hint)) {
|
|
1931
|
+
debug("User cancelled hint entry, returning to review menu");
|
|
1932
|
+
return null;
|
|
1933
|
+
}
|
|
1934
|
+
const hintValue = String(hint).trim();
|
|
1935
|
+
debug("Regenerating with hint:", hintValue);
|
|
1936
|
+
try {
|
|
1937
|
+
const newMessage = await regenerate(hintValue);
|
|
1938
|
+
debug("Regenerated message:", newMessage);
|
|
1939
|
+
return newMessage;
|
|
1940
|
+
} catch (err) {
|
|
1941
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
1942
|
+
debug("Regeneration failed:", errMsg);
|
|
1943
|
+
log.warn(red(`Regeneration failed: ${errMsg}`));
|
|
1944
|
+
return null;
|
|
1945
|
+
}
|
|
1946
|
+
}
|
|
1947
|
+
async function reviewCommitMessage(message, options) {
|
|
1948
|
+
const { select } = await import("@clack/prompts");
|
|
1949
|
+
while (true) {
|
|
1950
|
+
const reviewOptions = [{
|
|
1951
|
+
label: "Use as-is",
|
|
1952
|
+
value: "use"
|
|
1953
|
+
}, {
|
|
1954
|
+
label: "Edit",
|
|
1955
|
+
value: "edit"
|
|
1956
|
+
}];
|
|
1957
|
+
if (options?.regenerate) reviewOptions.push({
|
|
1958
|
+
label: "Regenerate with hint",
|
|
1959
|
+
value: "regenerate"
|
|
1960
|
+
});
|
|
1961
|
+
reviewOptions.push({
|
|
1962
|
+
label: "Cancel",
|
|
1963
|
+
value: "cancel"
|
|
1964
|
+
});
|
|
1965
|
+
const review = await select({
|
|
1966
|
+
message: `Review commit message:\n\n ${bold(message)}\n`,
|
|
1967
|
+
options: reviewOptions
|
|
1968
|
+
});
|
|
1969
|
+
if (isCancel(review) || review === "cancel") {
|
|
1970
|
+
debug("User cancelled at review step");
|
|
1971
|
+
return null;
|
|
1972
|
+
}
|
|
1973
|
+
if (review === "use") {
|
|
1974
|
+
debug("User accepted message");
|
|
1975
|
+
return message;
|
|
1976
|
+
}
|
|
1977
|
+
if (review === "edit") message = await handleEdit(message) ?? message;
|
|
1978
|
+
if (review === "regenerate" && options?.regenerate) message = await handleRegenerate(options.regenerate) ?? message;
|
|
1979
|
+
}
|
|
1980
|
+
}
|
|
1981
|
+
//#endregion
|
|
1667
1982
|
//#region src/ui/check-failure-menu.ts
|
|
1668
1983
|
const MAX_TSC_DIAGNOSTICS = 3;
|
|
1669
1984
|
const MAX_ESLINT_DIAGNOSTICS = 3;
|
|
@@ -1793,9 +2108,9 @@ function truncate(message, maxLength) {
|
|
|
1793
2108
|
async function showCheckFailureMenu(errors, rawStderr, onRetry) {
|
|
1794
2109
|
debug("showCheckFailureMenu: %d errors", errors.length);
|
|
1795
2110
|
let clipboardCopied = false;
|
|
1796
|
-
p.note(formatCheckFailureSummary(errors), red("Pre-commit check failed"));
|
|
2111
|
+
p$1.note(formatCheckFailureSummary(errors), red("Pre-commit check failed"));
|
|
1797
2112
|
while (true) {
|
|
1798
|
-
const choice = await p.select({
|
|
2113
|
+
const choice = await p$1.select({
|
|
1799
2114
|
message: "What do you want to do?",
|
|
1800
2115
|
options: [
|
|
1801
2116
|
{
|
|
@@ -1821,292 +2136,100 @@ async function showCheckFailureMenu(errors, rawStderr, onRetry) {
|
|
|
1821
2136
|
value: "cancel"
|
|
1822
2137
|
}
|
|
1823
2138
|
]
|
|
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");
|
|
1933
|
-
}
|
|
1934
|
-
//#endregion
|
|
1935
|
-
//#region src/ui/recovery-menu.ts
|
|
1936
|
-
async function showRecoveryMenu(errors, onRetry, onSkipHooks, onRestage, message, rawStderr) {
|
|
1937
|
-
debug("showRecoveryMenu: %d errors", errors.length);
|
|
1938
|
-
let clipboardCopied = false;
|
|
1939
|
-
let showNote = true;
|
|
1940
|
-
while (true) {
|
|
1941
|
-
if (showNote) {
|
|
1942
|
-
p.note(errors.map((e) => ` ${red("•")} [${e.tool}] ${e.message}`).join("\n"), red(bold("Pre-commit hook failed")));
|
|
1943
|
-
showNote = false;
|
|
1944
|
-
}
|
|
1945
|
-
const choice = await p.select({
|
|
1946
|
-
message: "What do you want to do?",
|
|
1947
|
-
options: [
|
|
1948
|
-
{
|
|
1949
|
-
label: clipboardCopied ? `${green("✓")} Copy error report to clipboard` : "Copy error report to clipboard",
|
|
1950
|
-
value: "clipboard",
|
|
1951
|
-
hint: clipboardCopied ? "Copied!" : "Paste into another terminal for an AI agent"
|
|
1952
|
-
},
|
|
1953
|
-
{
|
|
1954
|
-
label: "View full error output",
|
|
1955
|
-
value: "view",
|
|
1956
|
-
hint: "Show the raw stderr from hooks"
|
|
1957
|
-
},
|
|
1958
|
-
{
|
|
1959
|
-
label: "Skip hooks and commit (--no-verify)",
|
|
1960
|
-
value: "skip",
|
|
1961
|
-
hint: "Commit anyway, fix later"
|
|
1962
|
-
},
|
|
1963
|
-
{
|
|
1964
|
-
label: "Re-stage files and retry",
|
|
1965
|
-
value: "restage",
|
|
1966
|
-
hint: "Pick up fixes from another terminal"
|
|
1967
|
-
},
|
|
1968
|
-
{
|
|
1969
|
-
label: "Edit commit message",
|
|
1970
|
-
value: "edit",
|
|
1971
|
-
hint: "Modify the message before retrying"
|
|
1972
|
-
},
|
|
1973
|
-
{
|
|
1974
|
-
label: "Cancel",
|
|
1975
|
-
value: "cancel"
|
|
1976
|
-
}
|
|
1977
|
-
]
|
|
1978
|
-
});
|
|
1979
|
-
if (p.isCancel(choice)) {
|
|
1980
|
-
debug("showRecoveryMenu: user cancelled");
|
|
1981
|
-
p.outro(yellow("Cancelled. Message cached for --retry."));
|
|
1982
|
-
return "cancelled";
|
|
1983
|
-
}
|
|
1984
|
-
debug("showRecoveryMenu: user chose %s", choice);
|
|
1985
|
-
switch (choice) {
|
|
1986
|
-
case "clipboard":
|
|
1987
|
-
if (await copyToClipboard(rawStderr)) {
|
|
1988
|
-
clipboardCopied = true;
|
|
1989
|
-
p.log.step(green("Copied to clipboard."));
|
|
1990
|
-
} else p.log.warn(red("No clipboard tool found. Install xclip, wl-copy, or xsel."));
|
|
1991
|
-
continue;
|
|
1992
|
-
case "view":
|
|
1993
|
-
p.note(rawStderr.trim() || "(no raw output)", "Full error output");
|
|
1994
|
-
showNote = true;
|
|
1995
|
-
continue;
|
|
1996
|
-
case "skip":
|
|
1997
|
-
p.log.info(yellow("Committing with --no-verify..."));
|
|
1998
|
-
if (await onSkipHooks(message)) {
|
|
1999
|
-
p.outro(green("Committed (hooks skipped)."));
|
|
2000
|
-
return "committed";
|
|
2001
|
-
} else {
|
|
2002
|
-
p.outro(red("Commit failed even with --no-verify."));
|
|
2003
|
-
return "failed";
|
|
2004
|
-
}
|
|
2005
|
-
case "restage":
|
|
2006
|
-
p.log.info(cyan("Re-staging and retrying..."));
|
|
2007
|
-
if (await onRestage()) {
|
|
2008
|
-
p.outro(green("Committed successfully."));
|
|
2009
|
-
return "committed";
|
|
2010
|
-
}
|
|
2011
|
-
showNote = true;
|
|
2012
|
-
continue;
|
|
2013
|
-
case "edit": {
|
|
2014
|
-
const edited = await p.text({
|
|
2015
|
-
message: "Edit commit message:",
|
|
2016
|
-
initialValue: message,
|
|
2017
|
-
validate: (v) => v?.trim() ? void 0 : "Message cannot be empty"
|
|
2018
|
-
});
|
|
2019
|
-
if (p.isCancel(edited)) {
|
|
2020
|
-
p.outro(yellow("Cancelled. Message cached for --retry."));
|
|
2021
|
-
return "cancelled";
|
|
2022
|
-
}
|
|
2023
|
-
if (await onRetry()) {
|
|
2024
|
-
p.outro(green("Committed successfully."));
|
|
2025
|
-
return "committed";
|
|
2026
|
-
} else {
|
|
2027
|
-
p.outro(red("Commit failed again."));
|
|
2028
|
-
return "failed";
|
|
2029
|
-
}
|
|
2030
|
-
}
|
|
2139
|
+
});
|
|
2140
|
+
if (p$1.isCancel(choice)) {
|
|
2141
|
+
debug("showCheckFailureMenu: user cancelled");
|
|
2142
|
+
return "cancelled";
|
|
2143
|
+
}
|
|
2144
|
+
debug("showCheckFailureMenu: user chose %s", choice);
|
|
2145
|
+
switch (choice) {
|
|
2146
|
+
case "copy":
|
|
2147
|
+
if (await copyToClipboard(rawStderr)) {
|
|
2148
|
+
clipboardCopied = true;
|
|
2149
|
+
p$1.log.step(green("Copied to clipboard."));
|
|
2150
|
+
} else p$1.log.warn(red("No clipboard tool found. Install xclip, wl-copy, or xsel."));
|
|
2151
|
+
continue;
|
|
2152
|
+
case "view":
|
|
2153
|
+
p$1.note(rawStderr.trim() || "(no raw output)", "Full error output");
|
|
2154
|
+
continue;
|
|
2155
|
+
case "retry":
|
|
2156
|
+
if (onRetry) return "retried";
|
|
2157
|
+
return "retried";
|
|
2158
|
+
case "skip":
|
|
2159
|
+
p$1.log.info("Skipping checks and proceeding with commit...");
|
|
2160
|
+
return "skipped";
|
|
2031
2161
|
case "cancel":
|
|
2032
|
-
p.outro(dim("
|
|
2162
|
+
p$1.outro(dim("Cancelled."));
|
|
2033
2163
|
return "cancelled";
|
|
2034
2164
|
}
|
|
2035
2165
|
}
|
|
2036
2166
|
}
|
|
2037
2167
|
//#endregion
|
|
2038
|
-
//#region src/ui/
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
const { log, text } = await import("@clack/prompts");
|
|
2056
|
-
const hint = await text({
|
|
2057
|
-
message: "Describe what this commit is about to guide regeneration:",
|
|
2058
|
-
validate: (v) => v?.trim() ? void 0 : "Hint cannot be empty"
|
|
2059
|
-
});
|
|
2060
|
-
if (isCancel(hint)) {
|
|
2061
|
-
debug("User cancelled hint entry, returning to review menu");
|
|
2062
|
-
return null;
|
|
2063
|
-
}
|
|
2064
|
-
const hintValue = String(hint).trim();
|
|
2065
|
-
debug("Regenerating with hint:", hintValue);
|
|
2066
|
-
try {
|
|
2067
|
-
const newMessage = await regenerate(hintValue);
|
|
2068
|
-
debug("Regenerated message:", newMessage);
|
|
2069
|
-
return newMessage;
|
|
2070
|
-
} catch (err) {
|
|
2071
|
-
const errMsg = err instanceof Error ? err.message : String(err);
|
|
2072
|
-
debug("Regeneration failed:", errMsg);
|
|
2073
|
-
log.warn(red(`Regeneration failed: ${errMsg}`));
|
|
2074
|
-
return null;
|
|
2168
|
+
//#region src/ui/check-summary.ts
|
|
2169
|
+
/**
|
|
2170
|
+
* Stop a check spinner with a per-tool summary of the check results.
|
|
2171
|
+
*
|
|
2172
|
+
* - On success: stops with "All checks passed" and prints a `✓ tool` line
|
|
2173
|
+
* for each result.
|
|
2174
|
+
* - On failure: stops with "N checks failed" (pluralized). Raw error output
|
|
2175
|
+
* is intentionally NOT printed here — callers handle failure display
|
|
2176
|
+
* (menu, raw print, etc.).
|
|
2177
|
+
*/
|
|
2178
|
+
function stopCheckSpinner(spinner, results) {
|
|
2179
|
+
if (results.ok) {
|
|
2180
|
+
spinner.stop("All checks passed");
|
|
2181
|
+
if (results.results.length > 0) log.info(results.results.map((r) => ` ${green("✓")} ${r.tool}`).join("\n"));
|
|
2182
|
+
} else {
|
|
2183
|
+
const failed = results.results.filter((r) => !r.ok);
|
|
2184
|
+
spinner.stop(`${failed.length} check${failed.length !== 1 ? "s" : ""} failed`);
|
|
2075
2185
|
}
|
|
2076
2186
|
}
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2187
|
+
//#endregion
|
|
2188
|
+
//#region src/commands/check-phase.ts
|
|
2189
|
+
/**
|
|
2190
|
+
* Run user-defined pre-commit checks with an interactive failure menu.
|
|
2191
|
+
*
|
|
2192
|
+
* Single entry point for the check-execution pipeline shared by `runPreCommitChecks`
|
|
2193
|
+
* (post-staging, normal commit flow) and `runAutoGroupFlow` (pre-staging, auto-group
|
|
2194
|
+
* flow). Encapsulates: detectConfig guard → spinner → runAllChecks → retry loop with
|
|
2195
|
+
* `showCheckFailureMenu`.
|
|
2196
|
+
*
|
|
2197
|
+
* Caller responsibilities:
|
|
2198
|
+
* - Skip when `noCheck` is set (caller's policy).
|
|
2199
|
+
* - Skip when there are no files to check (caller has the file list context).
|
|
2200
|
+
* - Derive `files` in **repo-root-relative** form so they match `.cmintrc` globs.
|
|
2201
|
+
* Post-staging callers should use `getStagedFiles()`; pre-staging callers should
|
|
2202
|
+
* use `resolveToRepoRoot()` since the index doesn't yet contain those paths.
|
|
2203
|
+
*
|
|
2204
|
+
* Returns the outcome so the caller can decide how to handle cancellation
|
|
2205
|
+
* (`process.exit(1)` in the commit flow, `return "cancelled"` in auto-group).
|
|
2206
|
+
*/
|
|
2207
|
+
async function runCheckPhaseInteractive(repoRoot, files, timeout, onRetry) {
|
|
2208
|
+
if (!await detectConfig(repoRoot)) return "passed";
|
|
2209
|
+
debug("Running user checks on %d files...", files.length);
|
|
2210
|
+
const ck = spinner();
|
|
2211
|
+
ck.start("Running checks...");
|
|
2212
|
+
let checkResults = await runAllChecks(repoRoot, files, timeout);
|
|
2213
|
+
stopCheckSpinner(ck, checkResults);
|
|
2214
|
+
debug("Check results: ok=%s, count=%d", checkResults.ok, checkResults.results.length);
|
|
2215
|
+
while (!checkResults.ok) {
|
|
2216
|
+
const rawOutput = checkResults.results.filter((r) => !r.ok).map((r) => `[${r.tool}]\n${r.stdout}\n${r.stderr}`.trim()).join("\n\n");
|
|
2217
|
+
const menuResult = await showCheckFailureMenu(parseCheckErrors(rawOutput), rawOutput, async () => {
|
|
2218
|
+
return (await runAllChecks(repoRoot, files, timeout)).ok;
|
|
2098
2219
|
});
|
|
2099
|
-
if (
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2220
|
+
if (menuResult === "cancelled") return "cancelled";
|
|
2221
|
+
if (menuResult === "retried") {
|
|
2222
|
+
debug("Re-running checks after retry...");
|
|
2223
|
+
if (onRetry) await onRetry();
|
|
2224
|
+
ck.start("Running checks...");
|
|
2225
|
+
checkResults = await runAllChecks(repoRoot, files, timeout);
|
|
2226
|
+
stopCheckSpinner(ck, checkResults);
|
|
2227
|
+
debug("Retry check results: ok=%s, count=%d", checkResults.ok, checkResults.results.length);
|
|
2228
|
+
continue;
|
|
2106
2229
|
}
|
|
2107
|
-
|
|
2108
|
-
if (review === "regenerate" && options?.regenerate) message = await handleRegenerate(options.regenerate) ?? message;
|
|
2230
|
+
break;
|
|
2109
2231
|
}
|
|
2232
|
+
return checkResults.ok ? "passed" : "skipped";
|
|
2110
2233
|
}
|
|
2111
2234
|
//#endregion
|
|
2112
2235
|
//#region src/commands/auto-group.ts
|
|
@@ -2137,33 +2260,7 @@ async function runAutoGroupFlow(changedFiles, flags) {
|
|
|
2137
2260
|
}
|
|
2138
2261
|
if (!flags.noCheck) {
|
|
2139
2262
|
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
|
-
}
|
|
2263
|
+
if (await runCheckPhaseInteractive(await getRepoRoot(), await resolveToRepoRoot(included.filter((f) => f.status !== "D").map((f) => f.path)), 6e4) === "cancelled") return "cancelled";
|
|
2167
2264
|
}
|
|
2168
2265
|
const config = await readConfig();
|
|
2169
2266
|
const resolvedProvider = config.provider ?? "groq";
|
|
@@ -2192,7 +2289,9 @@ async function runAutoGroupFlow(changedFiles, flags) {
|
|
|
2192
2289
|
const validatedGroups = validateGroups((await generateGroups(included, await getProviderApiKey(provider), getModelForProvider(config, provider, PROVIDER_CONFIGS[provider].defaultModel), config.timeout ? parseInt(config.timeout, 10) : void 0, provider, config.proxy)).groups, included);
|
|
2193
2290
|
s.stop("Files analyzed");
|
|
2194
2291
|
showGroupedFiles(validatedGroups, included);
|
|
2195
|
-
|
|
2292
|
+
const autoAccept = await getAutoAccept();
|
|
2293
|
+
const skipPrompts = flags.auto || autoAccept;
|
|
2294
|
+
if (skipPrompts) debug("Skipping grouping confirmation (auto=%s autoAccept=%s)", flags.auto, autoAccept);
|
|
2196
2295
|
else if (!await showGroupingConfirmation(validatedGroups, excluded)) {
|
|
2197
2296
|
outro(dim("Cancelled."));
|
|
2198
2297
|
return "cancelled";
|
|
@@ -2218,7 +2317,7 @@ async function runAutoGroupFlow(changedFiles, flags) {
|
|
|
2218
2317
|
}
|
|
2219
2318
|
s.stop("Message generated");
|
|
2220
2319
|
log.info(dim(message));
|
|
2221
|
-
if (
|
|
2320
|
+
if (skipPrompts) debug("Accepting generated message (auto=%s autoAccept=%s)", flags.auto, autoAccept);
|
|
2222
2321
|
else {
|
|
2223
2322
|
const reviewed = await reviewCommitMessage(message, { regenerate: async (hint) => {
|
|
2224
2323
|
const combinedHint = flags.hint ? `${flags.hint}\n${hint}` : hint;
|
|
@@ -2401,7 +2500,7 @@ async function agentCommand(flags) {
|
|
|
2401
2500
|
const repoRoot = await getRepoRoot();
|
|
2402
2501
|
if (await detectConfig(repoRoot)) {
|
|
2403
2502
|
debug("Running user checks on changed files...");
|
|
2404
|
-
const checkResults = await runAllChecks(repoRoot, changedFiles.filter((f) => f.status !== "D").map((f) => f.path), 6e4);
|
|
2503
|
+
const checkResults = await runAllChecks(repoRoot, await resolveToRepoRoot(changedFiles.filter((f) => f.status !== "D").map((f) => f.path)), 6e4);
|
|
2405
2504
|
if (!checkResults.ok) {
|
|
2406
2505
|
const errorMessages = checkResults.results.filter((r) => !r.ok).map((r) => `[${r.tool}]\n${r.stdout}\n${r.stderr}`.trim()).filter(Boolean);
|
|
2407
2506
|
const parsed = parseCheckErrors(errorMessages.join("\n\n"));
|
|
@@ -3003,30 +3102,30 @@ function formatDetection(tools) {
|
|
|
3003
3102
|
async function setupCmintrcCommand(cwd = process.cwd()) {
|
|
3004
3103
|
debug("setupCmintrcCommand: starting in %s", cwd);
|
|
3005
3104
|
const tools = await detectTools(cwd);
|
|
3006
|
-
p.log.info(`Detected tools in ${bold(cwd)}:`);
|
|
3007
|
-
p.log.message(formatDetection(tools));
|
|
3008
|
-
if (!Object.values(tools).some(Boolean)) p.log.warn("No recognized tools found. Writing an empty config to fill in manually.");
|
|
3009
|
-
else if (tools.biome && tools.eslint) p.log.warn(yellow("Both biome and eslint detected — using biome (remove this line to switch)."));
|
|
3105
|
+
p$1.log.info(`Detected tools in ${bold(cwd)}:`);
|
|
3106
|
+
p$1.log.message(formatDetection(tools));
|
|
3107
|
+
if (!Object.values(tools).some(Boolean)) p$1.log.warn("No recognized tools found. Writing an empty config to fill in manually.");
|
|
3108
|
+
else if (tools.biome && tools.eslint) p$1.log.warn(yellow("Both biome and eslint detected — using biome (remove this line to switch)."));
|
|
3010
3109
|
const fileName = pickFileName(tools);
|
|
3011
3110
|
const filePath = join(cwd, fileName);
|
|
3012
3111
|
if (await exists(filePath)) {
|
|
3013
|
-
const overwrite = await p.confirm({ message: `${fileName} already exists. Overwrite?` });
|
|
3014
|
-
if (p.isCancel(overwrite) || !overwrite) {
|
|
3015
|
-
p.log.info(dim("Cancelled — existing file left untouched."));
|
|
3112
|
+
const overwrite = await p$1.confirm({ message: `${fileName} already exists. Overwrite?` });
|
|
3113
|
+
if (p$1.isCancel(overwrite) || !overwrite) {
|
|
3114
|
+
p$1.log.info(dim("Cancelled — existing file left untouched."));
|
|
3016
3115
|
return;
|
|
3017
3116
|
}
|
|
3018
3117
|
}
|
|
3019
3118
|
const content = buildCmintrcContent(tools);
|
|
3020
|
-
p.log.info(dim(`\nPreview of ${fileName}:`));
|
|
3021
|
-
p.log.message(dim(content));
|
|
3022
|
-
const confirm = await p.confirm({ message: `Write ${fileName}?` });
|
|
3023
|
-
if (p.isCancel(confirm) || !confirm) {
|
|
3024
|
-
p.log.info(dim("Cancelled."));
|
|
3119
|
+
p$1.log.info(dim(`\nPreview of ${fileName}:`));
|
|
3120
|
+
p$1.log.message(dim(content));
|
|
3121
|
+
const confirm = await p$1.confirm({ message: `Write ${fileName}?` });
|
|
3122
|
+
if (p$1.isCancel(confirm) || !confirm) {
|
|
3123
|
+
p$1.log.info(dim("Cancelled."));
|
|
3025
3124
|
return;
|
|
3026
3125
|
}
|
|
3027
3126
|
await writeFile(filePath, content, "utf-8");
|
|
3028
3127
|
debug("setupCmintrcCommand: wrote %s", filePath);
|
|
3029
|
-
p.log.success(green(`Wrote ${fileName}`));
|
|
3128
|
+
p$1.log.success(green(`Wrote ${fileName}`));
|
|
3030
3129
|
}
|
|
3031
3130
|
/** Project-local marker file that suppresses the preflight prompt forever. */
|
|
3032
3131
|
const SKIP_SETUP_MARKER = ".cmint-skip-setup";
|
|
@@ -3067,7 +3166,7 @@ async function runPreflightSetupPrompt(cwd) {
|
|
|
3067
3166
|
debug("preflight: project not auto-configurable, skipping prompt");
|
|
3068
3167
|
return;
|
|
3069
3168
|
}
|
|
3070
|
-
const choice = await p.select({
|
|
3169
|
+
const choice = await p$1.select({
|
|
3071
3170
|
message: "No .cmintrc found. Run setup to create one from detected tools?",
|
|
3072
3171
|
options: [
|
|
3073
3172
|
{
|
|
@@ -3084,23 +3183,698 @@ async function runPreflightSetupPrompt(cwd) {
|
|
|
3084
3183
|
}
|
|
3085
3184
|
]
|
|
3086
3185
|
});
|
|
3087
|
-
if (p.isCancel(choice)) {
|
|
3186
|
+
if (p$1.isCancel(choice)) {
|
|
3088
3187
|
debug("preflight: user cancelled prompt");
|
|
3089
3188
|
return;
|
|
3090
3189
|
}
|
|
3091
3190
|
if (choice === "never") {
|
|
3092
3191
|
await writeSkipSetupMarker(cwd);
|
|
3093
|
-
p.log.info(dim(`Won't ask again. Delete ${SKIP_SETUP_MARKER} to re-enable.`));
|
|
3192
|
+
p$1.log.info(dim(`Won't ask again. Delete ${SKIP_SETUP_MARKER} to re-enable.`));
|
|
3094
3193
|
return;
|
|
3095
3194
|
}
|
|
3096
3195
|
if (choice === "no") {
|
|
3097
|
-
p.log.info(dim("Skipping .cmintrc setup."));
|
|
3196
|
+
p$1.log.info(dim("Skipping .cmintrc setup."));
|
|
3098
3197
|
return;
|
|
3099
3198
|
}
|
|
3100
3199
|
debug("preflight: user chose yes, running setup");
|
|
3101
3200
|
await setupCmintrcCommand(cwd);
|
|
3102
3201
|
}
|
|
3103
3202
|
//#endregion
|
|
3203
|
+
//#region node_modules/fast-string-truncated-width/dist/utils.js
|
|
3204
|
+
const getCodePointsLength = (() => {
|
|
3205
|
+
const SURROGATE_PAIR_RE = /[\uD800-\uDBFF][\uDC00-\uDFFF]/g;
|
|
3206
|
+
return (input) => {
|
|
3207
|
+
let surrogatePairsNr = 0;
|
|
3208
|
+
SURROGATE_PAIR_RE.lastIndex = 0;
|
|
3209
|
+
while (SURROGATE_PAIR_RE.test(input)) surrogatePairsNr += 1;
|
|
3210
|
+
return input.length - surrogatePairsNr;
|
|
3211
|
+
};
|
|
3212
|
+
})();
|
|
3213
|
+
const isFullWidth = (x) => {
|
|
3214
|
+
return x === 12288 || x >= 65281 && x <= 65376 || x >= 65504 && x <= 65510;
|
|
3215
|
+
};
|
|
3216
|
+
const isWideNotCJKTNotEmoji = (x) => {
|
|
3217
|
+
return x === 8987 || x === 9001 || x >= 12272 && x <= 12287 || x >= 12289 && x <= 12350 || x >= 12441 && x <= 12543 || x >= 12549 && x <= 12591 || x >= 12593 && x <= 12686 || x >= 12688 && x <= 12771 || x >= 12783 && x <= 12830 || x >= 12832 && x <= 12871 || x >= 12880 && x <= 19903 || x >= 65040 && x <= 65049 || x >= 65072 && x <= 65106 || x >= 65108 && x <= 65126 || x >= 65128 && x <= 65131 || x >= 127488 && x <= 127490 || x >= 127504 && x <= 127547 || x >= 127552 && x <= 127560 || x >= 131072 && x <= 196605 || x >= 196608 && x <= 262141;
|
|
3218
|
+
};
|
|
3219
|
+
//#endregion
|
|
3220
|
+
//#region node_modules/fast-string-truncated-width/dist/index.js
|
|
3221
|
+
const ANSI_RE = /[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]|\u001b\]8;[^;]*;.*?(?:\u0007|\u001b\u005c)/y;
|
|
3222
|
+
const CONTROL_RE = /[\x00-\x08\x0A-\x1F\x7F-\x9F]{1,1000}/y;
|
|
3223
|
+
const CJKT_WIDE_RE = /(?:(?![\uFF61-\uFF9F\uFF00-\uFFEF])[\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}\p{Script=Hangul}\p{Script=Tangut}]){1,1000}/uy;
|
|
3224
|
+
const TAB_RE = /\t{1,1000}/y;
|
|
3225
|
+
const EMOJI_RE = /[\u{1F1E6}-\u{1F1FF}]{2}|\u{1F3F4}[\u{E0061}-\u{E007A}]{2}[\u{E0030}-\u{E0039}\u{E0061}-\u{E007A}]{1,3}\u{E007F}|(?:\p{Emoji}\uFE0F\u20E3?|\p{Emoji_Modifier_Base}\p{Emoji_Modifier}?|\p{Emoji_Presentation})(?:\u200D(?:\p{Emoji_Modifier_Base}\p{Emoji_Modifier}?|\p{Emoji_Presentation}|\p{Emoji}\uFE0F\u20E3?))*/uy;
|
|
3226
|
+
const LATIN_RE = /(?:[\x20-\x7E\xA0-\xFF](?!\uFE0F)){1,1000}/y;
|
|
3227
|
+
const MODIFIER_RE = /\p{M}+/gu;
|
|
3228
|
+
const NO_TRUNCATION$1 = {
|
|
3229
|
+
limit: Infinity,
|
|
3230
|
+
ellipsis: ""
|
|
3231
|
+
};
|
|
3232
|
+
const getStringTruncatedWidth = (input, truncationOptions = {}, widthOptions = {}) => {
|
|
3233
|
+
const LIMIT = truncationOptions.limit ?? Infinity;
|
|
3234
|
+
const ELLIPSIS = truncationOptions.ellipsis ?? "";
|
|
3235
|
+
const ELLIPSIS_WIDTH = truncationOptions?.ellipsisWidth ?? (ELLIPSIS ? getStringTruncatedWidth(ELLIPSIS, NO_TRUNCATION$1, widthOptions).width : 0);
|
|
3236
|
+
const ANSI_WIDTH = 0;
|
|
3237
|
+
const CONTROL_WIDTH = widthOptions.controlWidth ?? 0;
|
|
3238
|
+
const TAB_WIDTH = widthOptions.tabWidth ?? 8;
|
|
3239
|
+
const EMOJI_WIDTH = widthOptions.emojiWidth ?? 2;
|
|
3240
|
+
const FULL_WIDTH_WIDTH = 2;
|
|
3241
|
+
const REGULAR_WIDTH = widthOptions.regularWidth ?? 1;
|
|
3242
|
+
const WIDE_WIDTH = widthOptions.wideWidth ?? FULL_WIDTH_WIDTH;
|
|
3243
|
+
const PARSE_BLOCKS = [
|
|
3244
|
+
[LATIN_RE, REGULAR_WIDTH],
|
|
3245
|
+
[ANSI_RE, ANSI_WIDTH],
|
|
3246
|
+
[CONTROL_RE, CONTROL_WIDTH],
|
|
3247
|
+
[TAB_RE, TAB_WIDTH],
|
|
3248
|
+
[EMOJI_RE, EMOJI_WIDTH],
|
|
3249
|
+
[CJKT_WIDE_RE, WIDE_WIDTH]
|
|
3250
|
+
];
|
|
3251
|
+
let indexPrev = 0;
|
|
3252
|
+
let index = 0;
|
|
3253
|
+
let length = input.length;
|
|
3254
|
+
let lengthExtra = 0;
|
|
3255
|
+
let truncationEnabled = false;
|
|
3256
|
+
let truncationIndex = length;
|
|
3257
|
+
let truncationLimit = Math.max(0, LIMIT - ELLIPSIS_WIDTH);
|
|
3258
|
+
let unmatchedStart = 0;
|
|
3259
|
+
let unmatchedEnd = 0;
|
|
3260
|
+
let width = 0;
|
|
3261
|
+
let widthExtra = 0;
|
|
3262
|
+
outer: while (true) {
|
|
3263
|
+
if (unmatchedEnd > unmatchedStart || index >= length && index > indexPrev) {
|
|
3264
|
+
const unmatched = input.slice(unmatchedStart, unmatchedEnd) || input.slice(indexPrev, index);
|
|
3265
|
+
lengthExtra = 0;
|
|
3266
|
+
for (const char of unmatched.replaceAll(MODIFIER_RE, "")) {
|
|
3267
|
+
const codePoint = char.codePointAt(0) || 0;
|
|
3268
|
+
if (isFullWidth(codePoint)) widthExtra = FULL_WIDTH_WIDTH;
|
|
3269
|
+
else if (isWideNotCJKTNotEmoji(codePoint)) widthExtra = WIDE_WIDTH;
|
|
3270
|
+
else widthExtra = REGULAR_WIDTH;
|
|
3271
|
+
if (width + widthExtra > truncationLimit) truncationIndex = Math.min(truncationIndex, Math.max(unmatchedStart, indexPrev) + lengthExtra);
|
|
3272
|
+
if (width + widthExtra > LIMIT) {
|
|
3273
|
+
truncationEnabled = true;
|
|
3274
|
+
break outer;
|
|
3275
|
+
}
|
|
3276
|
+
lengthExtra += char.length;
|
|
3277
|
+
width += widthExtra;
|
|
3278
|
+
}
|
|
3279
|
+
unmatchedStart = unmatchedEnd = 0;
|
|
3280
|
+
}
|
|
3281
|
+
if (index >= length) break outer;
|
|
3282
|
+
for (let i = 0, l = PARSE_BLOCKS.length; i < l; i++) {
|
|
3283
|
+
const [BLOCK_RE, BLOCK_WIDTH] = PARSE_BLOCKS[i];
|
|
3284
|
+
BLOCK_RE.lastIndex = index;
|
|
3285
|
+
if (BLOCK_RE.test(input)) {
|
|
3286
|
+
lengthExtra = BLOCK_RE === CJKT_WIDE_RE ? getCodePointsLength(input.slice(index, BLOCK_RE.lastIndex)) : BLOCK_RE === EMOJI_RE ? 1 : BLOCK_RE.lastIndex - index;
|
|
3287
|
+
widthExtra = lengthExtra * BLOCK_WIDTH;
|
|
3288
|
+
if (width + widthExtra > truncationLimit) truncationIndex = Math.min(truncationIndex, index + Math.floor((truncationLimit - width) / BLOCK_WIDTH));
|
|
3289
|
+
if (width + widthExtra > LIMIT) {
|
|
3290
|
+
truncationEnabled = true;
|
|
3291
|
+
break outer;
|
|
3292
|
+
}
|
|
3293
|
+
width += widthExtra;
|
|
3294
|
+
unmatchedStart = indexPrev;
|
|
3295
|
+
unmatchedEnd = index;
|
|
3296
|
+
index = indexPrev = BLOCK_RE.lastIndex;
|
|
3297
|
+
continue outer;
|
|
3298
|
+
}
|
|
3299
|
+
}
|
|
3300
|
+
index += 1;
|
|
3301
|
+
}
|
|
3302
|
+
return {
|
|
3303
|
+
width: truncationEnabled ? truncationLimit : width,
|
|
3304
|
+
index: truncationEnabled ? truncationIndex : length,
|
|
3305
|
+
truncated: truncationEnabled,
|
|
3306
|
+
ellipsed: truncationEnabled && LIMIT >= ELLIPSIS_WIDTH
|
|
3307
|
+
};
|
|
3308
|
+
};
|
|
3309
|
+
//#endregion
|
|
3310
|
+
//#region node_modules/fast-string-width/dist/index.js
|
|
3311
|
+
const NO_TRUNCATION = {
|
|
3312
|
+
limit: Infinity,
|
|
3313
|
+
ellipsis: "",
|
|
3314
|
+
ellipsisWidth: 0
|
|
3315
|
+
};
|
|
3316
|
+
const fastStringWidth = (input, options = {}) => {
|
|
3317
|
+
return getStringTruncatedWidth(input, NO_TRUNCATION, options).width;
|
|
3318
|
+
};
|
|
3319
|
+
//#endregion
|
|
3320
|
+
//#region node_modules/fast-wrap-ansi/lib/main.js
|
|
3321
|
+
const ESC = "\x1B";
|
|
3322
|
+
const CSI = "";
|
|
3323
|
+
const END_CODE = 39;
|
|
3324
|
+
const ANSI_ESCAPE_BELL = "\x07";
|
|
3325
|
+
const ANSI_CSI = "[";
|
|
3326
|
+
const ANSI_OSC = "]";
|
|
3327
|
+
const ANSI_SGR_TERMINATOR = "m";
|
|
3328
|
+
const ANSI_ESCAPE_LINK = `${ANSI_OSC}8;;`;
|
|
3329
|
+
const GROUP_REGEX = new RegExp(`(?:\\${ANSI_CSI}(?<code>\\d+)m|\\${ANSI_ESCAPE_LINK}(?<uri>.*)${ANSI_ESCAPE_BELL})`, "y");
|
|
3330
|
+
const getClosingCode = (openingCode) => {
|
|
3331
|
+
if (openingCode >= 30 && openingCode <= 37) return 39;
|
|
3332
|
+
if (openingCode >= 90 && openingCode <= 97) return 39;
|
|
3333
|
+
if (openingCode >= 40 && openingCode <= 47) return 49;
|
|
3334
|
+
if (openingCode >= 100 && openingCode <= 107) return 49;
|
|
3335
|
+
if (openingCode === 1 || openingCode === 2) return 22;
|
|
3336
|
+
if (openingCode === 3) return 23;
|
|
3337
|
+
if (openingCode === 4) return 24;
|
|
3338
|
+
if (openingCode === 7) return 27;
|
|
3339
|
+
if (openingCode === 8) return 28;
|
|
3340
|
+
if (openingCode === 9) return 29;
|
|
3341
|
+
if (openingCode === 0) return 0;
|
|
3342
|
+
};
|
|
3343
|
+
const wrapAnsiCode = (code) => `${ESC}${ANSI_CSI}${code}${ANSI_SGR_TERMINATOR}`;
|
|
3344
|
+
const wrapAnsiHyperlink = (url) => `${ESC}${ANSI_ESCAPE_LINK}${url}${ANSI_ESCAPE_BELL}`;
|
|
3345
|
+
const wrapWord = (rows, word, columns) => {
|
|
3346
|
+
const characters = word[Symbol.iterator]();
|
|
3347
|
+
let isInsideEscape = false;
|
|
3348
|
+
let isInsideLinkEscape = false;
|
|
3349
|
+
let lastRow = rows.at(-1);
|
|
3350
|
+
let visible = lastRow === void 0 ? 0 : fastStringWidth(lastRow);
|
|
3351
|
+
let currentCharacter = characters.next();
|
|
3352
|
+
let nextCharacter = characters.next();
|
|
3353
|
+
let rawCharacterIndex = 0;
|
|
3354
|
+
while (!currentCharacter.done) {
|
|
3355
|
+
const character = currentCharacter.value;
|
|
3356
|
+
const characterLength = fastStringWidth(character);
|
|
3357
|
+
if (visible + characterLength <= columns) rows[rows.length - 1] += character;
|
|
3358
|
+
else {
|
|
3359
|
+
rows.push(character);
|
|
3360
|
+
visible = 0;
|
|
3361
|
+
}
|
|
3362
|
+
if (character === ESC || character === CSI) {
|
|
3363
|
+
isInsideEscape = true;
|
|
3364
|
+
isInsideLinkEscape = word.startsWith(ANSI_ESCAPE_LINK, rawCharacterIndex + 1);
|
|
3365
|
+
}
|
|
3366
|
+
if (isInsideEscape) {
|
|
3367
|
+
if (isInsideLinkEscape) {
|
|
3368
|
+
if (character === ANSI_ESCAPE_BELL) {
|
|
3369
|
+
isInsideEscape = false;
|
|
3370
|
+
isInsideLinkEscape = false;
|
|
3371
|
+
}
|
|
3372
|
+
} else if (character === ANSI_SGR_TERMINATOR) isInsideEscape = false;
|
|
3373
|
+
} else {
|
|
3374
|
+
visible += characterLength;
|
|
3375
|
+
if (visible === columns && !nextCharacter.done) {
|
|
3376
|
+
rows.push("");
|
|
3377
|
+
visible = 0;
|
|
3378
|
+
}
|
|
3379
|
+
}
|
|
3380
|
+
currentCharacter = nextCharacter;
|
|
3381
|
+
nextCharacter = characters.next();
|
|
3382
|
+
rawCharacterIndex += character.length;
|
|
3383
|
+
}
|
|
3384
|
+
lastRow = rows.at(-1);
|
|
3385
|
+
if (!visible && lastRow !== void 0 && lastRow.length && rows.length > 1) rows[rows.length - 2] += rows.pop();
|
|
3386
|
+
};
|
|
3387
|
+
const stringVisibleTrimSpacesRight = (string) => {
|
|
3388
|
+
const words = string.split(" ");
|
|
3389
|
+
let last = words.length;
|
|
3390
|
+
while (last) {
|
|
3391
|
+
if (fastStringWidth(words[last - 1])) break;
|
|
3392
|
+
last--;
|
|
3393
|
+
}
|
|
3394
|
+
if (last === words.length) return string;
|
|
3395
|
+
return words.slice(0, last).join(" ") + words.slice(last).join("");
|
|
3396
|
+
};
|
|
3397
|
+
const exec = (string, columns, options = {}) => {
|
|
3398
|
+
if (options.trim !== false && string.trim() === "") return "";
|
|
3399
|
+
let returnValue = "";
|
|
3400
|
+
let escapeCode;
|
|
3401
|
+
let escapeUrl;
|
|
3402
|
+
const words = string.split(" ");
|
|
3403
|
+
let rows = [""];
|
|
3404
|
+
let rowLength = 0;
|
|
3405
|
+
for (let index = 0; index < words.length; index++) {
|
|
3406
|
+
const word = words[index];
|
|
3407
|
+
if (options.trim !== false) {
|
|
3408
|
+
const row = rows.at(-1) ?? "";
|
|
3409
|
+
const trimmed = row.trimStart();
|
|
3410
|
+
if (row.length !== trimmed.length) {
|
|
3411
|
+
rows[rows.length - 1] = trimmed;
|
|
3412
|
+
rowLength = fastStringWidth(trimmed);
|
|
3413
|
+
}
|
|
3414
|
+
}
|
|
3415
|
+
if (index !== 0) {
|
|
3416
|
+
if (rowLength >= columns && (options.wordWrap === false || options.trim === false)) {
|
|
3417
|
+
rows.push("");
|
|
3418
|
+
rowLength = 0;
|
|
3419
|
+
}
|
|
3420
|
+
if (rowLength || options.trim === false) {
|
|
3421
|
+
rows[rows.length - 1] += " ";
|
|
3422
|
+
rowLength++;
|
|
3423
|
+
}
|
|
3424
|
+
}
|
|
3425
|
+
const wordLength = fastStringWidth(word);
|
|
3426
|
+
if (options.hard && wordLength > columns) {
|
|
3427
|
+
const remainingColumns = columns - rowLength;
|
|
3428
|
+
const breaksStartingThisLine = 1 + Math.floor((wordLength - remainingColumns - 1) / columns);
|
|
3429
|
+
if (Math.floor((wordLength - 1) / columns) < breaksStartingThisLine) rows.push("");
|
|
3430
|
+
wrapWord(rows, word, columns);
|
|
3431
|
+
rowLength = fastStringWidth(rows.at(-1) ?? "");
|
|
3432
|
+
continue;
|
|
3433
|
+
}
|
|
3434
|
+
if (rowLength + wordLength > columns && rowLength && wordLength) {
|
|
3435
|
+
if (options.wordWrap === false && rowLength < columns) {
|
|
3436
|
+
wrapWord(rows, word, columns);
|
|
3437
|
+
rowLength = fastStringWidth(rows.at(-1) ?? "");
|
|
3438
|
+
continue;
|
|
3439
|
+
}
|
|
3440
|
+
rows.push("");
|
|
3441
|
+
rowLength = 0;
|
|
3442
|
+
}
|
|
3443
|
+
if (rowLength + wordLength > columns && options.wordWrap === false) {
|
|
3444
|
+
wrapWord(rows, word, columns);
|
|
3445
|
+
rowLength = fastStringWidth(rows.at(-1) ?? "");
|
|
3446
|
+
continue;
|
|
3447
|
+
}
|
|
3448
|
+
rows[rows.length - 1] += word;
|
|
3449
|
+
rowLength += wordLength;
|
|
3450
|
+
}
|
|
3451
|
+
if (options.trim !== false) rows = rows.map((row) => stringVisibleTrimSpacesRight(row));
|
|
3452
|
+
const preString = rows.join("\n");
|
|
3453
|
+
let inSurrogate = false;
|
|
3454
|
+
for (let i = 0; i < preString.length; i++) {
|
|
3455
|
+
const character = preString[i];
|
|
3456
|
+
returnValue += character;
|
|
3457
|
+
if (!inSurrogate) {
|
|
3458
|
+
inSurrogate = character >= "\ud800" && character <= "\udbff";
|
|
3459
|
+
if (inSurrogate) continue;
|
|
3460
|
+
} else inSurrogate = false;
|
|
3461
|
+
if (character === ESC || character === CSI) {
|
|
3462
|
+
GROUP_REGEX.lastIndex = i + 1;
|
|
3463
|
+
const groups = GROUP_REGEX.exec(preString)?.groups;
|
|
3464
|
+
if (groups?.code !== void 0) {
|
|
3465
|
+
const code = Number.parseFloat(groups.code);
|
|
3466
|
+
escapeCode = code === END_CODE ? void 0 : code;
|
|
3467
|
+
} else if (groups?.uri !== void 0) escapeUrl = groups.uri.length === 0 ? void 0 : groups.uri;
|
|
3468
|
+
}
|
|
3469
|
+
if (preString[i + 1] === "\n") {
|
|
3470
|
+
if (escapeUrl) returnValue += wrapAnsiHyperlink("");
|
|
3471
|
+
const closingCode = escapeCode ? getClosingCode(escapeCode) : void 0;
|
|
3472
|
+
if (escapeCode && closingCode) returnValue += wrapAnsiCode(closingCode);
|
|
3473
|
+
} else if (character === "\n") {
|
|
3474
|
+
if (escapeCode && getClosingCode(escapeCode)) returnValue += wrapAnsiCode(escapeCode);
|
|
3475
|
+
if (escapeUrl) returnValue += wrapAnsiHyperlink(escapeUrl);
|
|
3476
|
+
}
|
|
3477
|
+
}
|
|
3478
|
+
return returnValue;
|
|
3479
|
+
};
|
|
3480
|
+
const CRLF_OR_LF = /\r?\n/;
|
|
3481
|
+
function wrapAnsi(string, columns, options) {
|
|
3482
|
+
return String(string).normalize().split(CRLF_OR_LF).map((line) => exec(line, columns, options)).join("\n");
|
|
3483
|
+
}
|
|
3484
|
+
//#endregion
|
|
3485
|
+
//#region node_modules/@clack/core/dist/index.mjs
|
|
3486
|
+
var import_src = (/* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
3487
|
+
const ESC = "\x1B";
|
|
3488
|
+
const CSI = `${ESC}[`;
|
|
3489
|
+
const beep = "\x07";
|
|
3490
|
+
const cursor = {
|
|
3491
|
+
to(x, y) {
|
|
3492
|
+
if (!y) return `${CSI}${x + 1}G`;
|
|
3493
|
+
return `${CSI}${y + 1};${x + 1}H`;
|
|
3494
|
+
},
|
|
3495
|
+
move(x, y) {
|
|
3496
|
+
let ret = "";
|
|
3497
|
+
if (x < 0) ret += `${CSI}${-x}D`;
|
|
3498
|
+
else if (x > 0) ret += `${CSI}${x}C`;
|
|
3499
|
+
if (y < 0) ret += `${CSI}${-y}A`;
|
|
3500
|
+
else if (y > 0) ret += `${CSI}${y}B`;
|
|
3501
|
+
return ret;
|
|
3502
|
+
},
|
|
3503
|
+
up: (count = 1) => `${CSI}${count}A`,
|
|
3504
|
+
down: (count = 1) => `${CSI}${count}B`,
|
|
3505
|
+
forward: (count = 1) => `${CSI}${count}C`,
|
|
3506
|
+
backward: (count = 1) => `${CSI}${count}D`,
|
|
3507
|
+
nextLine: (count = 1) => `${CSI}E`.repeat(count),
|
|
3508
|
+
prevLine: (count = 1) => `${CSI}F`.repeat(count),
|
|
3509
|
+
left: `${CSI}G`,
|
|
3510
|
+
hide: `${CSI}?25l`,
|
|
3511
|
+
show: `${CSI}?25h`,
|
|
3512
|
+
save: `${ESC}7`,
|
|
3513
|
+
restore: `${ESC}8`
|
|
3514
|
+
};
|
|
3515
|
+
module.exports = {
|
|
3516
|
+
cursor,
|
|
3517
|
+
scroll: {
|
|
3518
|
+
up: (count = 1) => `${CSI}S`.repeat(count),
|
|
3519
|
+
down: (count = 1) => `${CSI}T`.repeat(count)
|
|
3520
|
+
},
|
|
3521
|
+
erase: {
|
|
3522
|
+
screen: `${CSI}2J`,
|
|
3523
|
+
up: (count = 1) => `${CSI}1J`.repeat(count),
|
|
3524
|
+
down: (count = 1) => `${CSI}J`.repeat(count),
|
|
3525
|
+
line: `${CSI}2K`,
|
|
3526
|
+
lineEnd: `${CSI}K`,
|
|
3527
|
+
lineStart: `${CSI}1K`,
|
|
3528
|
+
lines(count) {
|
|
3529
|
+
let clear = "";
|
|
3530
|
+
for (let i = 0; i < count; i++) clear += this.line + (i < count - 1 ? cursor.up() : "");
|
|
3531
|
+
if (count) clear += cursor.left;
|
|
3532
|
+
return clear;
|
|
3533
|
+
}
|
|
3534
|
+
},
|
|
3535
|
+
beep
|
|
3536
|
+
};
|
|
3537
|
+
})))();
|
|
3538
|
+
function f(r, t, s) {
|
|
3539
|
+
if (!s.some((o) => !o.disabled)) return r;
|
|
3540
|
+
const e = r + t, i = Math.max(s.length - 1, 0), n = e < 0 ? i : e > i ? 0 : e;
|
|
3541
|
+
return s[n].disabled ? f(n, t < 0 ? -1 : 1, s) : n;
|
|
3542
|
+
}
|
|
3543
|
+
const h = {
|
|
3544
|
+
actions: new Set([
|
|
3545
|
+
"up",
|
|
3546
|
+
"down",
|
|
3547
|
+
"left",
|
|
3548
|
+
"right",
|
|
3549
|
+
"space",
|
|
3550
|
+
"enter",
|
|
3551
|
+
"cancel"
|
|
3552
|
+
]),
|
|
3553
|
+
aliases: new Map([
|
|
3554
|
+
["k", "up"],
|
|
3555
|
+
["j", "down"],
|
|
3556
|
+
["h", "left"],
|
|
3557
|
+
["l", "right"],
|
|
3558
|
+
["", "cancel"],
|
|
3559
|
+
["escape", "cancel"]
|
|
3560
|
+
]),
|
|
3561
|
+
messages: {
|
|
3562
|
+
cancel: "Canceled",
|
|
3563
|
+
error: "Something went wrong"
|
|
3564
|
+
},
|
|
3565
|
+
withGuide: !0,
|
|
3566
|
+
date: {
|
|
3567
|
+
monthNames: [...[
|
|
3568
|
+
"January",
|
|
3569
|
+
"February",
|
|
3570
|
+
"March",
|
|
3571
|
+
"April",
|
|
3572
|
+
"May",
|
|
3573
|
+
"June",
|
|
3574
|
+
"July",
|
|
3575
|
+
"August",
|
|
3576
|
+
"September",
|
|
3577
|
+
"October",
|
|
3578
|
+
"November",
|
|
3579
|
+
"December"
|
|
3580
|
+
]],
|
|
3581
|
+
messages: {
|
|
3582
|
+
required: "Please enter a valid date",
|
|
3583
|
+
invalidMonth: "There are only 12 months in a year",
|
|
3584
|
+
invalidDay: (r, t) => `There are only ${r} days in ${t}`,
|
|
3585
|
+
afterMin: (r) => `Date must be on or after ${r.toISOString().slice(0, 10)}`,
|
|
3586
|
+
beforeMax: (r) => `Date must be on or before ${r.toISOString().slice(0, 10)}`
|
|
3587
|
+
}
|
|
3588
|
+
}
|
|
3589
|
+
};
|
|
3590
|
+
function C(r, t) {
|
|
3591
|
+
if (typeof r == "string") return h.aliases.get(r) === t;
|
|
3592
|
+
for (const s of r) if (s !== void 0 && C(s, t)) return !0;
|
|
3593
|
+
return !1;
|
|
3594
|
+
}
|
|
3595
|
+
function z(r, t) {
|
|
3596
|
+
if (r === t) return;
|
|
3597
|
+
const s = r.split(`
|
|
3598
|
+
`), e = t.split(`
|
|
3599
|
+
`), i = Math.max(s.length, e.length), n = [];
|
|
3600
|
+
for (let o = 0; o < i; o++) s[o] !== e[o] && n.push(o);
|
|
3601
|
+
return {
|
|
3602
|
+
lines: n,
|
|
3603
|
+
numLinesBefore: s.length,
|
|
3604
|
+
numLinesAfter: e.length,
|
|
3605
|
+
numLines: i
|
|
3606
|
+
};
|
|
3607
|
+
}
|
|
3608
|
+
globalThis.process.platform.startsWith("win");
|
|
3609
|
+
const k = Symbol("clack:cancel");
|
|
3610
|
+
function q(r) {
|
|
3611
|
+
return r === k;
|
|
3612
|
+
}
|
|
3613
|
+
function w(r, t) {
|
|
3614
|
+
const s = r;
|
|
3615
|
+
s.isTTY && s.setRawMode(t);
|
|
3616
|
+
}
|
|
3617
|
+
const L = (r) => "rows" in r && typeof r.rows == "number" ? r.rows : 20;
|
|
3618
|
+
let m = class {
|
|
3619
|
+
input;
|
|
3620
|
+
output;
|
|
3621
|
+
_abortSignal;
|
|
3622
|
+
rl;
|
|
3623
|
+
opts;
|
|
3624
|
+
_render;
|
|
3625
|
+
_track = !1;
|
|
3626
|
+
_prevFrame = "";
|
|
3627
|
+
_subscribers = /* @__PURE__ */ new Map();
|
|
3628
|
+
_cursor = 0;
|
|
3629
|
+
state = "initial";
|
|
3630
|
+
error = "";
|
|
3631
|
+
value;
|
|
3632
|
+
userInput = "";
|
|
3633
|
+
constructor(t, s = !0) {
|
|
3634
|
+
const { input: e = stdin, output: i = stdout, render: n, signal: o, ...u } = t;
|
|
3635
|
+
this.opts = u, this.onKeypress = this.onKeypress.bind(this), this.close = this.close.bind(this), this.render = this.render.bind(this), this._render = n.bind(this), this._track = s, this._abortSignal = o, this.input = e, this.output = i;
|
|
3636
|
+
}
|
|
3637
|
+
unsubscribe() {
|
|
3638
|
+
this._subscribers.clear();
|
|
3639
|
+
}
|
|
3640
|
+
setSubscriber(t, s) {
|
|
3641
|
+
const e = this._subscribers.get(t) ?? [];
|
|
3642
|
+
e.push(s), this._subscribers.set(t, e);
|
|
3643
|
+
}
|
|
3644
|
+
on(t, s) {
|
|
3645
|
+
this.setSubscriber(t, { cb: s });
|
|
3646
|
+
}
|
|
3647
|
+
once(t, s) {
|
|
3648
|
+
this.setSubscriber(t, {
|
|
3649
|
+
cb: s,
|
|
3650
|
+
once: !0
|
|
3651
|
+
});
|
|
3652
|
+
}
|
|
3653
|
+
emit(t, ...s) {
|
|
3654
|
+
const e = this._subscribers.get(t) ?? [], i = [];
|
|
3655
|
+
for (const n of e) n.cb(...s), n.once && i.push(() => e.splice(e.indexOf(n), 1));
|
|
3656
|
+
for (const n of i) n();
|
|
3657
|
+
}
|
|
3658
|
+
prompt() {
|
|
3659
|
+
return new Promise((t) => {
|
|
3660
|
+
if (this._abortSignal) {
|
|
3661
|
+
if (this._abortSignal.aborted) return this.state = "cancel", this.close(), t(k);
|
|
3662
|
+
this._abortSignal.addEventListener("abort", () => {
|
|
3663
|
+
this.state = "cancel", this.close();
|
|
3664
|
+
}, { once: !0 });
|
|
3665
|
+
}
|
|
3666
|
+
this.rl = E.createInterface({
|
|
3667
|
+
input: this.input,
|
|
3668
|
+
tabSize: 2,
|
|
3669
|
+
prompt: "",
|
|
3670
|
+
escapeCodeTimeout: 50,
|
|
3671
|
+
terminal: !0
|
|
3672
|
+
}), this.rl.prompt(), this.opts.initialUserInput !== void 0 && this._setUserInput(this.opts.initialUserInput, !0), this.input.on("keypress", this.onKeypress), w(this.input, !0), this.output.on("resize", this.render), this.render(), this.once("submit", () => {
|
|
3673
|
+
this.output.write(import_src.cursor.show), this.output.off("resize", this.render), w(this.input, !1), t(this.value);
|
|
3674
|
+
}), this.once("cancel", () => {
|
|
3675
|
+
this.output.write(import_src.cursor.show), this.output.off("resize", this.render), w(this.input, !1), t(k);
|
|
3676
|
+
});
|
|
3677
|
+
});
|
|
3678
|
+
}
|
|
3679
|
+
_isActionKey(t, s) {
|
|
3680
|
+
return t === " ";
|
|
3681
|
+
}
|
|
3682
|
+
_shouldSubmit(t, s) {
|
|
3683
|
+
return !0;
|
|
3684
|
+
}
|
|
3685
|
+
_setValue(t) {
|
|
3686
|
+
this.value = t, this.emit("value", this.value);
|
|
3687
|
+
}
|
|
3688
|
+
_setUserInput(t, s) {
|
|
3689
|
+
this.userInput = t ?? "", this.emit("userInput", this.userInput), s && this._track && this.rl && (this.rl.write(this.userInput), this._cursor = this.rl.cursor);
|
|
3690
|
+
}
|
|
3691
|
+
_clearUserInput() {
|
|
3692
|
+
this.rl?.write(null, {
|
|
3693
|
+
ctrl: !0,
|
|
3694
|
+
name: "u"
|
|
3695
|
+
}), this._setUserInput("");
|
|
3696
|
+
}
|
|
3697
|
+
onKeypress(t, s) {
|
|
3698
|
+
if (this._track && s.name !== "return" && (s.name && this._isActionKey(t, s) && this.rl?.write(null, {
|
|
3699
|
+
ctrl: !0,
|
|
3700
|
+
name: "h"
|
|
3701
|
+
}), this._cursor = this.rl?.cursor ?? 0, this._setUserInput(this.rl?.line)), this.state === "error" && (this.state = "active"), s?.name && (!this._track && h.aliases.has(s.name) && this.emit("cursor", h.aliases.get(s.name)), h.actions.has(s.name) && this.emit("cursor", s.name)), t && (t.toLowerCase() === "y" || t.toLowerCase() === "n") && this.emit("confirm", t.toLowerCase() === "y"), this.emit("key", t?.toLowerCase(), s), s?.name === "return" && this._shouldSubmit(t, s)) {
|
|
3702
|
+
if (this.opts.validate) {
|
|
3703
|
+
const e = this.opts.validate(this.value);
|
|
3704
|
+
e && (this.error = e instanceof Error ? e.message : e, this.state = "error", this.rl?.write(this.userInput));
|
|
3705
|
+
}
|
|
3706
|
+
this.state !== "error" && (this.state = "submit");
|
|
3707
|
+
}
|
|
3708
|
+
C([
|
|
3709
|
+
t,
|
|
3710
|
+
s?.name,
|
|
3711
|
+
s?.sequence
|
|
3712
|
+
], "cancel") && (this.state = "cancel"), (this.state === "submit" || this.state === "cancel") && this.emit("finalize"), this.render(), (this.state === "submit" || this.state === "cancel") && this.close();
|
|
3713
|
+
}
|
|
3714
|
+
close() {
|
|
3715
|
+
this.input.unpipe(), this.input.removeListener("keypress", this.onKeypress), this.output.write(`
|
|
3716
|
+
`), w(this.input, !1), this.rl?.close(), this.rl = void 0, this.emit(`${this.state}`, this.value), this.unsubscribe();
|
|
3717
|
+
}
|
|
3718
|
+
restoreCursor() {
|
|
3719
|
+
const t = wrapAnsi(this._prevFrame, process.stdout.columns, {
|
|
3720
|
+
hard: !0,
|
|
3721
|
+
trim: !1
|
|
3722
|
+
}).split(`
|
|
3723
|
+
`).length - 1;
|
|
3724
|
+
this.output.write(import_src.cursor.move(-999, t * -1));
|
|
3725
|
+
}
|
|
3726
|
+
render() {
|
|
3727
|
+
const t = wrapAnsi(this._render(this) ?? "", process.stdout.columns, {
|
|
3728
|
+
hard: !0,
|
|
3729
|
+
trim: !1
|
|
3730
|
+
});
|
|
3731
|
+
if (t !== this._prevFrame) {
|
|
3732
|
+
if (this.state === "initial") this.output.write(import_src.cursor.hide);
|
|
3733
|
+
else {
|
|
3734
|
+
const s = z(this._prevFrame, t), e = L(this.output);
|
|
3735
|
+
if (this.restoreCursor(), s) {
|
|
3736
|
+
const i = Math.max(0, s.numLinesAfter - e), n = Math.max(0, s.numLinesBefore - e);
|
|
3737
|
+
let o = s.lines.find((u) => u >= i);
|
|
3738
|
+
if (o === void 0) {
|
|
3739
|
+
this._prevFrame = t;
|
|
3740
|
+
return;
|
|
3741
|
+
}
|
|
3742
|
+
if (s.lines.length === 1) {
|
|
3743
|
+
this.output.write(import_src.cursor.move(0, o - n)), this.output.write(import_src.erase.lines(1));
|
|
3744
|
+
const u = t.split(`
|
|
3745
|
+
`);
|
|
3746
|
+
this.output.write(u[o]), this._prevFrame = t, this.output.write(import_src.cursor.move(0, u.length - o - 1));
|
|
3747
|
+
return;
|
|
3748
|
+
} else if (s.lines.length > 1) {
|
|
3749
|
+
if (i < n) o = i;
|
|
3750
|
+
else {
|
|
3751
|
+
const a = o - n;
|
|
3752
|
+
a > 0 && this.output.write(import_src.cursor.move(0, a));
|
|
3753
|
+
}
|
|
3754
|
+
this.output.write(import_src.erase.down());
|
|
3755
|
+
const u = t.split(`
|
|
3756
|
+
`).slice(o);
|
|
3757
|
+
this.output.write(u.join(`
|
|
3758
|
+
`)), this._prevFrame = t;
|
|
3759
|
+
return;
|
|
3760
|
+
}
|
|
3761
|
+
}
|
|
3762
|
+
this.output.write(import_src.erase.down());
|
|
3763
|
+
}
|
|
3764
|
+
this.output.write(t), this.state === "initial" && (this.state = "active"), this._prevFrame = t;
|
|
3765
|
+
}
|
|
3766
|
+
}
|
|
3767
|
+
};
|
|
3768
|
+
var ut = class extends m {
|
|
3769
|
+
options;
|
|
3770
|
+
cursor = 0;
|
|
3771
|
+
get _selectedValue() {
|
|
3772
|
+
return this.options[this.cursor];
|
|
3773
|
+
}
|
|
3774
|
+
changeValue() {
|
|
3775
|
+
this.value = this._selectedValue.value;
|
|
3776
|
+
}
|
|
3777
|
+
constructor(t) {
|
|
3778
|
+
super(t, !1), this.options = t.options;
|
|
3779
|
+
const s = this.options.findIndex(({ value: i }) => i === t.initialValue), e = s === -1 ? 0 : s;
|
|
3780
|
+
this.cursor = this.options[e].disabled ? f(e, 1, this.options) : e, this.changeValue(), this.on("cursor", (i) => {
|
|
3781
|
+
switch (i) {
|
|
3782
|
+
case "left":
|
|
3783
|
+
case "up":
|
|
3784
|
+
this.cursor = f(this.cursor, -1, this.options);
|
|
3785
|
+
break;
|
|
3786
|
+
case "down":
|
|
3787
|
+
case "right":
|
|
3788
|
+
this.cursor = f(this.cursor, 1, this.options);
|
|
3789
|
+
break;
|
|
3790
|
+
}
|
|
3791
|
+
this.changeValue();
|
|
3792
|
+
});
|
|
3793
|
+
}
|
|
3794
|
+
};
|
|
3795
|
+
//#endregion
|
|
3796
|
+
//#region src/ui/auto-accept-select.ts
|
|
3797
|
+
const ON_LABEL = styleText("green", "ON");
|
|
3798
|
+
const OFF_LABEL = styleText("dim", "OFF");
|
|
3799
|
+
const HOTKEY_HINT = dim("(press `a` to toggle)");
|
|
3800
|
+
function renderStatus(autoAccept) {
|
|
3801
|
+
const label = autoAccept ? ON_LABEL : OFF_LABEL;
|
|
3802
|
+
return `${dim("⚡ Auto-accept:")} ${label} ${HOTKEY_HINT}`;
|
|
3803
|
+
}
|
|
3804
|
+
/** Render a single select option line. */
|
|
3805
|
+
function renderOption(opt, active) {
|
|
3806
|
+
const text = opt.label ?? String(opt.value);
|
|
3807
|
+
if (opt.disabled) return `${dim(S_RADIO_INACTIVE)} ${styleText(["strikethrough", "dim"], text)}`;
|
|
3808
|
+
if (active) {
|
|
3809
|
+
const hint = opt.hint ? ` ${dim(`(${opt.hint})`)}` : "";
|
|
3810
|
+
return `${green(S_RADIO_ACTIVE)} ${text}${hint}`;
|
|
3811
|
+
}
|
|
3812
|
+
return `${dim(S_RADIO_INACTIVE)} ${dim(text)}`;
|
|
3813
|
+
}
|
|
3814
|
+
/**
|
|
3815
|
+
* Select prompt with an inline `a`-hotkey toggle for auto-accept mode.
|
|
3816
|
+
*
|
|
3817
|
+
* Renders a normal select list plus a status line showing the current
|
|
3818
|
+
* auto-accept state. Pressing `a` flips the state in-place (the menu
|
|
3819
|
+
* re-renders) and fires `onToggle` so callers can persist the change.
|
|
3820
|
+
*
|
|
3821
|
+
* Returns `{ value, autoAccept }` on submit, or the clack cancel symbol
|
|
3822
|
+
* on cancel.
|
|
3823
|
+
*/
|
|
3824
|
+
async function selectWithAutoAccept(opts) {
|
|
3825
|
+
let autoAccept = opts.initialAutoAccept;
|
|
3826
|
+
const prompt = new ut({
|
|
3827
|
+
options: opts.options,
|
|
3828
|
+
input: opts.input,
|
|
3829
|
+
output: opts.output,
|
|
3830
|
+
render() {
|
|
3831
|
+
const sym = symbol(this.state);
|
|
3832
|
+
const statusLine = renderStatus(autoAccept);
|
|
3833
|
+
const header = `${sym} ${opts.message}\n${dim(S_BAR)} ${statusLine}`;
|
|
3834
|
+
switch (this.state) {
|
|
3835
|
+
case "submit": {
|
|
3836
|
+
const selected = this.options[this.cursor];
|
|
3837
|
+
const text = selected.label ?? String(selected.value);
|
|
3838
|
+
return `${header}\n${dim(S_BAR)} ${dim(text)}`;
|
|
3839
|
+
}
|
|
3840
|
+
case "cancel": {
|
|
3841
|
+
const selected = this.options[this.cursor];
|
|
3842
|
+
const text = selected.label ?? String(selected.value);
|
|
3843
|
+
return `${header}\n${dim(S_BAR)} ${styleText(["strikethrough", "dim"], text)}\n${dim(S_BAR_END)}`;
|
|
3844
|
+
}
|
|
3845
|
+
default: return [
|
|
3846
|
+
header,
|
|
3847
|
+
...limitOptions({
|
|
3848
|
+
cursor: this.cursor,
|
|
3849
|
+
options: this.options,
|
|
3850
|
+
style: (opt, active) => renderOption(opt, active),
|
|
3851
|
+
maxItems: 7,
|
|
3852
|
+
output: opts.output ?? process.stdout
|
|
3853
|
+
}).map((line) => `${dim(S_BAR)} ${line}`),
|
|
3854
|
+
`${dim(S_BAR_END)} ${dim("↑/↓ navigate • Enter confirm • `a` toggle auto-accept")}`
|
|
3855
|
+
].join("\n");
|
|
3856
|
+
}
|
|
3857
|
+
}
|
|
3858
|
+
});
|
|
3859
|
+
prompt.on("key", async (char) => {
|
|
3860
|
+
if (char === "a") {
|
|
3861
|
+
autoAccept = !autoAccept;
|
|
3862
|
+
debug("auto-accept toggled to %s", autoAccept);
|
|
3863
|
+
try {
|
|
3864
|
+
await opts.onToggle?.(autoAccept);
|
|
3865
|
+
} catch (err) {
|
|
3866
|
+
debug("onToggle threw (ignored): %s", err instanceof Error ? err.message : String(err));
|
|
3867
|
+
}
|
|
3868
|
+
}
|
|
3869
|
+
});
|
|
3870
|
+
const result = await prompt.prompt();
|
|
3871
|
+
if (q(result)) return result;
|
|
3872
|
+
return {
|
|
3873
|
+
value: result,
|
|
3874
|
+
autoAccept
|
|
3875
|
+
};
|
|
3876
|
+
}
|
|
3877
|
+
//#endregion
|
|
3104
3878
|
//#region src/ui/staging-menu.ts
|
|
3105
3879
|
async function showStagingMenu(files, hasChecks) {
|
|
3106
3880
|
debug("showStagingMenu: %d files", files.length);
|
|
@@ -3126,9 +3900,15 @@ async function showStagingMenu(files, hasChecks) {
|
|
|
3126
3900
|
if (lines.length > 0) lines.push("");
|
|
3127
3901
|
lines.push(yellow(bold("Changed:")), ...unstagedFiles.map((f) => ` ${statusLabel(f.status)} ${f.path}`));
|
|
3128
3902
|
}
|
|
3129
|
-
p.note(lines.join("\n"), `${files.length} file${files.length !== 1 ? "s" : ""}`);
|
|
3130
|
-
const
|
|
3903
|
+
p$1.note(lines.join("\n"), `${files.length} file${files.length !== 1 ? "s" : ""}`);
|
|
3904
|
+
const initialAutoAccept = await getAutoAccept();
|
|
3905
|
+
debug("showStagingMenu: initial auto-accept=%s", initialAutoAccept);
|
|
3906
|
+
const selectResult = await selectWithAutoAccept({
|
|
3131
3907
|
message: "Stage files for commit:",
|
|
3908
|
+
initialAutoAccept,
|
|
3909
|
+
onToggle: async (next) => {
|
|
3910
|
+
await setAutoAccept(next);
|
|
3911
|
+
},
|
|
3132
3912
|
options: [
|
|
3133
3913
|
{
|
|
3134
3914
|
label: "Auto-group into commits",
|
|
@@ -3160,7 +3940,13 @@ async function showStagingMenu(files, hasChecks) {
|
|
|
3160
3940
|
}
|
|
3161
3941
|
]
|
|
3162
3942
|
});
|
|
3163
|
-
if (
|
|
3943
|
+
if (typeof selectResult === "symbol") {
|
|
3944
|
+
debug("showStagingMenu: user cancelled (clack cancel symbol)");
|
|
3945
|
+
return null;
|
|
3946
|
+
}
|
|
3947
|
+
const choice = selectResult.value;
|
|
3948
|
+
debug("showStagingMenu: choice=%s autoAccept=%s", choice, selectResult.autoAccept);
|
|
3949
|
+
if (p$1.isCancel(choice) || choice === "cancel") return null;
|
|
3164
3950
|
if (choice === "autogroup") return "autogroup";
|
|
3165
3951
|
if (choice === "checks") return "checks";
|
|
3166
3952
|
if (choice === "staged") return "staged";
|
|
@@ -3168,7 +3954,7 @@ async function showStagingMenu(files, hasChecks) {
|
|
|
3168
3954
|
files: files.map((f) => f.path),
|
|
3169
3955
|
all: true
|
|
3170
3956
|
};
|
|
3171
|
-
const selected = await p.multiselect({
|
|
3957
|
+
const selected = await p$1.multiselect({
|
|
3172
3958
|
message: "Select files to stage:",
|
|
3173
3959
|
options: sorted.map((f) => ({
|
|
3174
3960
|
label: `${statusLabel(f.status)} ${f.path}`,
|
|
@@ -3176,7 +3962,7 @@ async function showStagingMenu(files, hasChecks) {
|
|
|
3176
3962
|
})),
|
|
3177
3963
|
required: true
|
|
3178
3964
|
});
|
|
3179
|
-
if (p.isCancel(selected)) return null;
|
|
3965
|
+
if (p$1.isCancel(selected)) return null;
|
|
3180
3966
|
return {
|
|
3181
3967
|
files: selected,
|
|
3182
3968
|
all: false
|
|
@@ -3206,7 +3992,7 @@ async function handleStaging(changedFiles, flags) {
|
|
|
3206
3992
|
}
|
|
3207
3993
|
if (stagingResult === "checks") {
|
|
3208
3994
|
await stageAll();
|
|
3209
|
-
const allFiles =
|
|
3995
|
+
const allFiles = await getStagedFiles();
|
|
3210
3996
|
if (await detectConfig(repoRoot)) {
|
|
3211
3997
|
const ckSpinner = spinner();
|
|
3212
3998
|
ckSpinner.start("Running checks...");
|
|
@@ -3245,33 +4031,13 @@ async function handleStaging(changedFiles, flags) {
|
|
|
3245
4031
|
async function runPreCommitChecks(changedFiles, noCheck) {
|
|
3246
4032
|
if (noCheck) return;
|
|
3247
4033
|
const checkRoot = await getRepoRoot();
|
|
3248
|
-
const stagedFileList =
|
|
4034
|
+
const stagedFileList = await getStagedFiles();
|
|
3249
4035
|
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);
|
|
4036
|
+
if (await runCheckPhaseInteractive(checkRoot, stagedFileList, 6e4, async () => {
|
|
4037
|
+
debug("Re-staging files before retry...");
|
|
4038
|
+
await stageAll();
|
|
4039
|
+
}) === "cancelled") process.exit(1);
|
|
4040
|
+
await restageFormatterModifications(changedFiles.filter((f) => f.staged && f.status !== "D").map((f) => f.path));
|
|
3275
4041
|
}
|
|
3276
4042
|
/**
|
|
3277
4043
|
* Re-stage staged files whose working-tree content diverged from the index after checks ran.
|
|
@@ -3395,24 +4161,29 @@ async function commitCommand(flags, version) {
|
|
|
3395
4161
|
}
|
|
3396
4162
|
s.stop("Message generated");
|
|
3397
4163
|
}
|
|
3398
|
-
|
|
3399
|
-
|
|
3400
|
-
|
|
3401
|
-
|
|
3402
|
-
|
|
3403
|
-
const
|
|
3404
|
-
|
|
3405
|
-
|
|
3406
|
-
|
|
3407
|
-
|
|
3408
|
-
|
|
4164
|
+
if (await getAutoAccept()) {
|
|
4165
|
+
debug("Auto-accept ON: skipping review step");
|
|
4166
|
+
log.info(message);
|
|
4167
|
+
} else {
|
|
4168
|
+
const reviewed = await reviewCommitMessage(message, { regenerate: async (hint) => {
|
|
4169
|
+
const combinedHint = flags.hint ? `${flags.hint}\n${hint}` : hint;
|
|
4170
|
+
debug("Regenerating with combined hint:", combinedHint);
|
|
4171
|
+
s.start("Regenerating commit message...");
|
|
4172
|
+
try {
|
|
4173
|
+
const newMessage = await generateMessage(diffResult.diff, combinedHint);
|
|
4174
|
+
s.stop("Message regenerated");
|
|
4175
|
+
return newMessage;
|
|
4176
|
+
} catch (err) {
|
|
4177
|
+
s.stop(red("Regeneration failed"));
|
|
4178
|
+
throw err;
|
|
4179
|
+
}
|
|
4180
|
+
} });
|
|
4181
|
+
if (reviewed === null) {
|
|
4182
|
+
outro(dim("Cancelled."));
|
|
4183
|
+
return;
|
|
3409
4184
|
}
|
|
3410
|
-
|
|
3411
|
-
if (reviewed === null) {
|
|
3412
|
-
outro(dim("Cancelled."));
|
|
3413
|
-
return;
|
|
4185
|
+
message = reviewed;
|
|
3414
4186
|
}
|
|
3415
|
-
message = reviewed;
|
|
3416
4187
|
await saveCachedCommit(repoRoot, message);
|
|
3417
4188
|
debug("Message cached for repo:", repoRoot);
|
|
3418
4189
|
s.start("Running pre-commit hooks...");
|
|
@@ -3452,7 +4223,7 @@ function getProvider(config) {
|
|
|
3452
4223
|
return isValidProvider(config.provider ?? "groq") ? config.provider : "groq";
|
|
3453
4224
|
}
|
|
3454
4225
|
async function promptProvider() {
|
|
3455
|
-
return p.select({
|
|
4226
|
+
return p$1.select({
|
|
3456
4227
|
message: "Select LLM provider:",
|
|
3457
4228
|
options: [
|
|
3458
4229
|
{
|
|
@@ -3475,24 +4246,24 @@ async function promptProvider() {
|
|
|
3475
4246
|
}
|
|
3476
4247
|
async function promptApiKey(provider) {
|
|
3477
4248
|
const keyName = PROVIDER_ENV_KEYS[provider];
|
|
3478
|
-
const result = await p.text({
|
|
4249
|
+
const result = await p$1.text({
|
|
3479
4250
|
message: `${formatProviderName(provider)} API key:`,
|
|
3480
4251
|
placeholder: "Paste your API key",
|
|
3481
4252
|
validate: (v) => !v?.trim() ? "API key cannot be empty" : void 0
|
|
3482
4253
|
});
|
|
3483
|
-
if (p.isCancel(result)) return result;
|
|
4254
|
+
if (p$1.isCancel(result)) return result;
|
|
3484
4255
|
await writeConfig({ [keyName]: result.toString().trim() });
|
|
3485
4256
|
debug("config: %s set", keyName);
|
|
3486
4257
|
return result;
|
|
3487
4258
|
}
|
|
3488
4259
|
async function promptTextSetting(label, configKey, currentValue, validate) {
|
|
3489
|
-
const result = await p.text({
|
|
4260
|
+
const result = await p$1.text({
|
|
3490
4261
|
message: label,
|
|
3491
4262
|
placeholder: currentValue ?? "",
|
|
3492
4263
|
initialValue: currentValue ?? "",
|
|
3493
4264
|
validate
|
|
3494
4265
|
});
|
|
3495
|
-
if (p.isCancel(result)) return result;
|
|
4266
|
+
if (p$1.isCancel(result)) return result;
|
|
3496
4267
|
await writeConfig({ [configKey]: result.toString().trim() });
|
|
3497
4268
|
debug("config: %s set to %s", configKey, result);
|
|
3498
4269
|
return result;
|
|
@@ -3506,7 +4277,7 @@ function getSettingHandlers(config) {
|
|
|
3506
4277
|
return {
|
|
3507
4278
|
provider: async () => {
|
|
3508
4279
|
const result = await promptProvider();
|
|
3509
|
-
if (p.isCancel(result)) return result;
|
|
4280
|
+
if (p$1.isCancel(result)) return result;
|
|
3510
4281
|
const newProvider = result;
|
|
3511
4282
|
const newDefaultModel = PROVIDER_CONFIGS[newProvider].defaultModel;
|
|
3512
4283
|
await writeConfig({
|
|
@@ -3517,7 +4288,7 @@ function getSettingHandlers(config) {
|
|
|
3517
4288
|
const keyName = PROVIDER_ENV_KEYS[newProvider];
|
|
3518
4289
|
if (!(await readConfig())[keyName]) {
|
|
3519
4290
|
const keyResult = await promptApiKey(newProvider);
|
|
3520
|
-
if (p.isCancel(keyResult)) return keyResult;
|
|
4291
|
+
if (p$1.isCancel(keyResult)) return keyResult;
|
|
3521
4292
|
}
|
|
3522
4293
|
},
|
|
3523
4294
|
apikey: async () => promptApiKey(provider),
|
|
@@ -3535,7 +4306,7 @@ async function handleEditSetting(setting, config) {
|
|
|
3535
4306
|
const handler = getSettingHandlers(config)[setting];
|
|
3536
4307
|
if (!handler) return false;
|
|
3537
4308
|
const result = await handler();
|
|
3538
|
-
return !p.isCancel(result);
|
|
4309
|
+
return !p$1.isCancel(result);
|
|
3539
4310
|
}
|
|
3540
4311
|
async function editSettingsLoop(initialConfig) {
|
|
3541
4312
|
let config = initialConfig;
|
|
@@ -3543,7 +4314,7 @@ async function editSettingsLoop(initialConfig) {
|
|
|
3543
4314
|
config = await readConfig();
|
|
3544
4315
|
const provider = getProvider(config);
|
|
3545
4316
|
const effectiveModel = getModelForProvider(config, provider, PROVIDER_CONFIGS[provider].defaultModel);
|
|
3546
|
-
const setting = await p.select({
|
|
4317
|
+
const setting = await p$1.select({
|
|
3547
4318
|
message: "Select a setting to edit:",
|
|
3548
4319
|
options: [
|
|
3549
4320
|
{
|
|
@@ -3584,17 +4355,17 @@ async function editSettingsLoop(initialConfig) {
|
|
|
3584
4355
|
}
|
|
3585
4356
|
]
|
|
3586
4357
|
});
|
|
3587
|
-
if (p.isCancel(setting) || setting === "done") break;
|
|
3588
|
-
if (await handleEditSetting(setting, config)) p.log.success(green("Updated."));
|
|
4358
|
+
if (p$1.isCancel(setting) || setting === "done") break;
|
|
4359
|
+
if (await handleEditSetting(setting, config)) p$1.log.success(green("Updated."));
|
|
3589
4360
|
}
|
|
3590
4361
|
}
|
|
3591
4362
|
async function configCommand() {
|
|
3592
4363
|
debug("configCommand: starting");
|
|
3593
|
-
p.intro(bold("🌿 commit-mint config"));
|
|
4364
|
+
p$1.intro(bold("🌿 commit-mint config"));
|
|
3594
4365
|
while (true) {
|
|
3595
4366
|
const config = await readConfig();
|
|
3596
|
-
p.note(buildConfigDisplay(config), "commit-mint config");
|
|
3597
|
-
const action = await p.select({
|
|
4367
|
+
p$1.note(buildConfigDisplay(config), "commit-mint config");
|
|
4368
|
+
const action = await p$1.select({
|
|
3598
4369
|
message: "What would you like to do?",
|
|
3599
4370
|
options: [
|
|
3600
4371
|
{
|
|
@@ -3611,14 +4382,14 @@ async function configCommand() {
|
|
|
3611
4382
|
}
|
|
3612
4383
|
]
|
|
3613
4384
|
});
|
|
3614
|
-
if (p.isCancel(action)) {
|
|
4385
|
+
if (p$1.isCancel(action)) {
|
|
3615
4386
|
debug("configCommand: cancelled at main menu");
|
|
3616
|
-
p.outro(dim("Cancelled."));
|
|
4387
|
+
p$1.outro(dim("Cancelled."));
|
|
3617
4388
|
return;
|
|
3618
4389
|
}
|
|
3619
4390
|
if (action === "done") {
|
|
3620
4391
|
debug("configCommand: done");
|
|
3621
|
-
p.outro("Config saved.");
|
|
4392
|
+
p$1.outro("Config saved.");
|
|
3622
4393
|
return;
|
|
3623
4394
|
}
|
|
3624
4395
|
if (action === "setup") {
|
|
@@ -3772,43 +4543,53 @@ async function runUpdate(pm, packageName = PACKAGE_NAME) {
|
|
|
3772
4543
|
* resolve normally so the CLI exits cleanly.
|
|
3773
4544
|
*/
|
|
3774
4545
|
async function updateCommand(currentVersion, flags) {
|
|
3775
|
-
p.intro("cmint update");
|
|
4546
|
+
p$1.intro("cmint update");
|
|
3776
4547
|
const pm = detectPackageManager(process.env.npm_config_user_agent);
|
|
3777
|
-
p.log.info(`Package manager: ${pm}`);
|
|
3778
|
-
p.log.message("Checking latest version...");
|
|
4548
|
+
p$1.log.info(`Package manager: ${pm}`);
|
|
4549
|
+
p$1.log.message("Checking latest version...");
|
|
3779
4550
|
const latest = await fetchLatestVersion();
|
|
3780
4551
|
if (latest === null) {
|
|
3781
|
-
p.outro(red("Could not reach the npm registry. Check your connection and try again."));
|
|
4552
|
+
p$1.outro(red("Could not reach the npm registry. Check your connection and try again."));
|
|
3782
4553
|
process.exit(1);
|
|
3783
4554
|
return;
|
|
3784
4555
|
}
|
|
3785
4556
|
if (!isUpdateAvailable(currentVersion, latest)) {
|
|
3786
|
-
p.outro(`Already up-to-date: v${currentVersion}`);
|
|
4557
|
+
p$1.outro(`Already up-to-date: v${currentVersion}`);
|
|
3787
4558
|
return;
|
|
3788
4559
|
}
|
|
3789
|
-
p.log.step(`${dim(currentVersion)} → ${green(latest)}`);
|
|
4560
|
+
p$1.log.step(`${dim(currentVersion)} → ${green(latest)}`);
|
|
3790
4561
|
const cmd = buildUpdateCommand(pm);
|
|
3791
4562
|
if (flags?.yes !== true) {
|
|
3792
|
-
const confirmed = await p.confirm({
|
|
4563
|
+
const confirmed = await p$1.confirm({
|
|
3793
4564
|
message: `Run \`${cmd}\`?`,
|
|
3794
4565
|
initialValue: true
|
|
3795
4566
|
});
|
|
3796
|
-
if (p.isCancel(confirmed) || !confirmed) {
|
|
3797
|
-
p.outro("Update cancelled.");
|
|
4567
|
+
if (p$1.isCancel(confirmed) || !confirmed) {
|
|
4568
|
+
p$1.outro("Update cancelled.");
|
|
3798
4569
|
return;
|
|
3799
4570
|
}
|
|
3800
4571
|
}
|
|
3801
|
-
p.log.message(`Running ${cyan(cmd)}...`);
|
|
4572
|
+
p$1.log.message(`Running ${cyan(cmd)}...`);
|
|
3802
4573
|
if (await runUpdate(pm)) {
|
|
3803
|
-
p.outro(green(`Updated to v${latest}`));
|
|
4574
|
+
p$1.outro(green(`Updated to v${latest}`));
|
|
3804
4575
|
return;
|
|
3805
4576
|
}
|
|
3806
|
-
p.outro(red("Update failed. See output above."));
|
|
4577
|
+
p$1.outro(red("Update failed. See output above."));
|
|
3807
4578
|
process.exit(1);
|
|
3808
4579
|
}
|
|
3809
4580
|
//#endregion
|
|
3810
4581
|
//#region src/cli.ts
|
|
3811
4582
|
const { version } = package_default;
|
|
4583
|
+
/** `cmint auto` subcommand handler — equivalent to `cmint --auto`. */
|
|
4584
|
+
async function handleAutoSubcommand(version) {
|
|
4585
|
+
writeSessionHeader();
|
|
4586
|
+
setDebug(false);
|
|
4587
|
+
commitCommand({
|
|
4588
|
+
auto: true,
|
|
4589
|
+
retry: false,
|
|
4590
|
+
agent: false
|
|
4591
|
+
}, version);
|
|
4592
|
+
}
|
|
3812
4593
|
cli({
|
|
3813
4594
|
name: "cmint",
|
|
3814
4595
|
version,
|
|
@@ -3866,6 +4647,12 @@ cli({
|
|
|
3866
4647
|
}, async (argv) => {
|
|
3867
4648
|
await logsCommand(argv.flags);
|
|
3868
4649
|
}),
|
|
4650
|
+
command({
|
|
4651
|
+
name: "auto",
|
|
4652
|
+
description: "Auto-group files into logical commits (alias for --auto)"
|
|
4653
|
+
}, async () => {
|
|
4654
|
+
await handleAutoSubcommand(version);
|
|
4655
|
+
}),
|
|
3869
4656
|
command({ name: "config" }, async () => {
|
|
3870
4657
|
await configCommand();
|
|
3871
4658
|
}),
|
|
@@ -3889,6 +4676,6 @@ cli({
|
|
|
3889
4676
|
else commitCommand(argv.flags, version);
|
|
3890
4677
|
});
|
|
3891
4678
|
//#endregion
|
|
3892
|
-
export {};
|
|
4679
|
+
export { handleAutoSubcommand };
|
|
3893
4680
|
|
|
3894
4681
|
//# sourceMappingURL=cli.mjs.map
|