@kyubiware/commit-mint 0.5.6 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +248 -141
- package/dist/cli.mjs +254 -11
- package/dist/cli.mjs.map +1 -1
- package/package.json +3 -2
package/dist/cli.mjs
CHANGED
|
@@ -28,7 +28,7 @@ var __exportAll = (all, no_symbols) => {
|
|
|
28
28
|
//#region package.json
|
|
29
29
|
var package_default = {
|
|
30
30
|
name: "@kyubiware/commit-mint",
|
|
31
|
-
version: "0.
|
|
31
|
+
version: "0.6.0",
|
|
32
32
|
description: "🌿 A commit tool that actually handles hook failures",
|
|
33
33
|
type: "module",
|
|
34
34
|
bin: { "cmint": "./dist/cli.mjs" },
|
|
@@ -47,7 +47,8 @@ var package_default = {
|
|
|
47
47
|
"release:patch": "bash scripts/release.sh patch",
|
|
48
48
|
"release:minor": "bash scripts/release.sh minor",
|
|
49
49
|
"release:major": "bash scripts/release.sh major",
|
|
50
|
-
"prepublishOnly": "npm run build"
|
|
50
|
+
"prepublishOnly": "npm run build",
|
|
51
|
+
"publish:cmint": "cd packages/cmint && npm publish"
|
|
51
52
|
},
|
|
52
53
|
keywords: [
|
|
53
54
|
"git",
|
|
@@ -1374,8 +1375,10 @@ function tryCopy(cmd, args, content) {
|
|
|
1374
1375
|
//#endregion
|
|
1375
1376
|
//#region src/ui/check-failure-menu.ts
|
|
1376
1377
|
const MAX_TSC_DIAGNOSTICS = 3;
|
|
1378
|
+
const MAX_ESLINT_DIAGNOSTICS = 3;
|
|
1377
1379
|
const MAX_SUMMARY_LINE_LENGTH = 120;
|
|
1378
1380
|
const TSC_DIAGNOSTIC = /^(.+?\.(?:ts|tsx|mts|cts|js|jsx|mjs|cjs))\((\d+),(\d+)\):\s+error\s+(TS\d+):\s+(.+)$/;
|
|
1381
|
+
const ESLINT_ERROR_LINE = /^\s*(\d+):(\d+)\s+(error|warning)\s+(.+)\s{2,}(\S+)\s*$/;
|
|
1379
1382
|
function formatCheckFailureSummary(errors) {
|
|
1380
1383
|
if (errors.length === 0) return "No check error details were parsed. View full output for details.";
|
|
1381
1384
|
return errors.map((error) => formatCheckErrorSummary(error)).join("\n");
|
|
@@ -1385,6 +1388,10 @@ function formatCheckErrorSummary(error) {
|
|
|
1385
1388
|
const diagnostics = extractTscDiagnostics(error.raw || error.message);
|
|
1386
1389
|
if (diagnostics.length > 0) return formatTscSummary(diagnostics);
|
|
1387
1390
|
}
|
|
1391
|
+
if (error.tool === "eslint") {
|
|
1392
|
+
const diagnostics = extractEslintDiagnostics(error.raw || error.message);
|
|
1393
|
+
if (diagnostics.length > 0) return formatEslintSummary(diagnostics);
|
|
1394
|
+
}
|
|
1388
1395
|
const message = firstMeaningfulLine(error.message || error.raw);
|
|
1389
1396
|
return ` ${red("•")} [${error.tool}] ${truncate(message, MAX_SUMMARY_LINE_LENGTH)}`;
|
|
1390
1397
|
}
|
|
@@ -1408,6 +1415,36 @@ function formatTscSummary(diagnostics) {
|
|
|
1408
1415
|
if (hidden > 0) lines.push(dim(` +${hidden} more TypeScript error${hidden !== 1 ? "s" : ""}. View full output for details.`));
|
|
1409
1416
|
return lines.join("\n");
|
|
1410
1417
|
}
|
|
1418
|
+
function extractEslintDiagnostics(raw) {
|
|
1419
|
+
const diagnostics = [];
|
|
1420
|
+
const lines = raw.split("\n");
|
|
1421
|
+
let currentFile = "";
|
|
1422
|
+
for (const line of lines) {
|
|
1423
|
+
if (!/^\s/.test(line) && line.includes("/") && !ESLINT_ERROR_LINE.test(line)) {
|
|
1424
|
+
currentFile = line.trim();
|
|
1425
|
+
continue;
|
|
1426
|
+
}
|
|
1427
|
+
const match = ESLINT_ERROR_LINE.exec(line);
|
|
1428
|
+
if (match) diagnostics.push({
|
|
1429
|
+
file: currentFile || "unknown",
|
|
1430
|
+
line: match[1] ?? "",
|
|
1431
|
+
column: match[2] ?? "",
|
|
1432
|
+
severity: match[3] ?? "",
|
|
1433
|
+
message: (match[4] ?? "").trim(),
|
|
1434
|
+
rule: match[5] ?? ""
|
|
1435
|
+
});
|
|
1436
|
+
}
|
|
1437
|
+
return diagnostics;
|
|
1438
|
+
}
|
|
1439
|
+
function formatEslintSummary(diagnostics) {
|
|
1440
|
+
const visible = diagnostics.slice(0, MAX_ESLINT_DIAGNOSTICS);
|
|
1441
|
+
const hidden = diagnostics.length - visible.length;
|
|
1442
|
+
const count = diagnostics.length;
|
|
1443
|
+
const noun = count === 1 ? "problem" : "problems";
|
|
1444
|
+
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)}`)];
|
|
1445
|
+
if (hidden > 0) lines.push(dim(` +${hidden} more ESLint ${hidden === 1 ? "problem" : "problems"}. View full output for details.`));
|
|
1446
|
+
return lines.join("\n");
|
|
1447
|
+
}
|
|
1411
1448
|
function firstMeaningfulLine(message) {
|
|
1412
1449
|
return message.split("\n").map((l) => l.trim()).find((l) => l.length > 0 && !l.startsWith(">") && !l.startsWith("ELIFECYCLE")) ?? message;
|
|
1413
1450
|
}
|
|
@@ -1873,6 +1910,199 @@ async function handleRetry() {
|
|
|
1873
1910
|
else process.exit(1);
|
|
1874
1911
|
}
|
|
1875
1912
|
//#endregion
|
|
1913
|
+
//#region src/commands/setup.ts
|
|
1914
|
+
/** Marker files for each tool. First match wins per tool. */
|
|
1915
|
+
const TOOL_MARKERS = {
|
|
1916
|
+
biome: ["biome.json", "biome.jsonc"],
|
|
1917
|
+
eslint: [
|
|
1918
|
+
"eslint.config.js",
|
|
1919
|
+
"eslint.config.mjs",
|
|
1920
|
+
"eslint.config.ts",
|
|
1921
|
+
"eslint.config.cjs",
|
|
1922
|
+
".eslintrc.js",
|
|
1923
|
+
".eslintrc.cjs",
|
|
1924
|
+
".eslintrc.json",
|
|
1925
|
+
".eslintrc.yml",
|
|
1926
|
+
".eslintrc.yaml",
|
|
1927
|
+
".eslintrc"
|
|
1928
|
+
],
|
|
1929
|
+
typescript: ["tsconfig.json"],
|
|
1930
|
+
vitest: [
|
|
1931
|
+
"vitest.config.js",
|
|
1932
|
+
"vitest.config.mts",
|
|
1933
|
+
"vitest.config.ts",
|
|
1934
|
+
"vitest.config.mjs"
|
|
1935
|
+
]
|
|
1936
|
+
};
|
|
1937
|
+
/** Indent for generated config — matches biome.json `indentStyle: "tab"`. */
|
|
1938
|
+
const TAB = " ";
|
|
1939
|
+
async function exists(path) {
|
|
1940
|
+
try {
|
|
1941
|
+
await access(path, constants.R_OK);
|
|
1942
|
+
return true;
|
|
1943
|
+
} catch {
|
|
1944
|
+
return false;
|
|
1945
|
+
}
|
|
1946
|
+
}
|
|
1947
|
+
/**
|
|
1948
|
+
* Scan a directory for marker files that indicate which tools the project uses.
|
|
1949
|
+
* Returns a map of tool name to detected status. Order within each tool's list
|
|
1950
|
+
* is priority order (first match wins).
|
|
1951
|
+
*/
|
|
1952
|
+
async function detectTools(cwd) {
|
|
1953
|
+
const result = {
|
|
1954
|
+
biome: false,
|
|
1955
|
+
eslint: false,
|
|
1956
|
+
typescript: false,
|
|
1957
|
+
vitest: false
|
|
1958
|
+
};
|
|
1959
|
+
for (const [tool, files] of Object.entries(TOOL_MARKERS)) for (const file of files) if (await exists(join(cwd, file))) {
|
|
1960
|
+
result[tool] = true;
|
|
1961
|
+
debug("setup: detected %s via %s", tool, file);
|
|
1962
|
+
break;
|
|
1963
|
+
}
|
|
1964
|
+
debug("setup: detection result %o", result);
|
|
1965
|
+
return result;
|
|
1966
|
+
}
|
|
1967
|
+
/**
|
|
1968
|
+
* Build the string content of a .cmintrc file from a detection result.
|
|
1969
|
+
* Returns tabs-indented TS/JS object literal with trailing commas. Biome is
|
|
1970
|
+
* preferred when both biome and eslint are present — overlapping globs would
|
|
1971
|
+
* cause both tools to run on the same files, which is wasteful and noisy.
|
|
1972
|
+
*/
|
|
1973
|
+
function buildCmintrcContent(tools) {
|
|
1974
|
+
const entries = [];
|
|
1975
|
+
if (tools.biome || tools.eslint) {
|
|
1976
|
+
const cmd = tools.biome ? "biome check --write --no-errors-on-unmatched --error-on-warnings" : "eslint --fix";
|
|
1977
|
+
const ext = tools.biome ? "{js,ts,json}" : "{js,ts}";
|
|
1978
|
+
entries.push(`${TAB}"*.${ext}": "${cmd}",`);
|
|
1979
|
+
}
|
|
1980
|
+
const tsChecks = [];
|
|
1981
|
+
if (tools.typescript) tsChecks.push("tsc --noEmit");
|
|
1982
|
+
if (tools.vitest) tsChecks.push("vitest run --passWithNoTests");
|
|
1983
|
+
if (tsChecks.length > 0) {
|
|
1984
|
+
const body = tsChecks.map((c) => `"${c}"`).join(", ");
|
|
1985
|
+
const fn = tsChecks.length === 1 ? `() => ${body}` : `() => [${body}]`;
|
|
1986
|
+
entries.push(`${TAB}"*.ts": ${fn},`);
|
|
1987
|
+
}
|
|
1988
|
+
if (entries.length === 0) return `export default {\n};\n`;
|
|
1989
|
+
return `export default {\n${entries.join("\n")}\n};\n`;
|
|
1990
|
+
}
|
|
1991
|
+
/** Choose the file extension based on whether the project uses TypeScript. */
|
|
1992
|
+
function pickFileName(tools) {
|
|
1993
|
+
return tools.typescript ? ".cmintrc.ts" : ".cmintrc";
|
|
1994
|
+
}
|
|
1995
|
+
function formatDetection(tools) {
|
|
1996
|
+
return Object.entries(tools).map(([tool, found]) => ` ${found ? green("✓") : dim("✗")} ${tool}`).join("\n");
|
|
1997
|
+
}
|
|
1998
|
+
/**
|
|
1999
|
+
* Interactive setup for `.cmintrc`. Detects biome/eslint/typescript/vitest in
|
|
2000
|
+
* the given directory, previews the generated config, and writes the file
|
|
2001
|
+
* after confirmation. Refuses to overwrite without explicit consent. Defaults
|
|
2002
|
+
* to `process.cwd()` when called from the `cmint config` menu; the preflight
|
|
2003
|
+
* caller passes the repo root explicitly.
|
|
2004
|
+
*/
|
|
2005
|
+
async function setupCmintrcCommand(cwd = process.cwd()) {
|
|
2006
|
+
debug("setupCmintrcCommand: starting in %s", cwd);
|
|
2007
|
+
const tools = await detectTools(cwd);
|
|
2008
|
+
p.log.info(`Detected tools in ${bold(cwd)}:`);
|
|
2009
|
+
p.log.message(formatDetection(tools));
|
|
2010
|
+
if (!Object.values(tools).some(Boolean)) p.log.warn("No recognized tools found. Writing an empty config to fill in manually.");
|
|
2011
|
+
else if (tools.biome && tools.eslint) p.log.warn(yellow("Both biome and eslint detected — using biome (remove this line to switch)."));
|
|
2012
|
+
const fileName = pickFileName(tools);
|
|
2013
|
+
const filePath = join(cwd, fileName);
|
|
2014
|
+
if (await exists(filePath)) {
|
|
2015
|
+
const overwrite = await p.confirm({ message: `${fileName} already exists. Overwrite?` });
|
|
2016
|
+
if (p.isCancel(overwrite) || !overwrite) {
|
|
2017
|
+
p.log.info(dim("Cancelled — existing file left untouched."));
|
|
2018
|
+
return;
|
|
2019
|
+
}
|
|
2020
|
+
}
|
|
2021
|
+
const content = buildCmintrcContent(tools);
|
|
2022
|
+
p.log.info(dim(`\nPreview of ${fileName}:`));
|
|
2023
|
+
p.log.message(dim(content));
|
|
2024
|
+
const confirm = await p.confirm({ message: `Write ${fileName}?` });
|
|
2025
|
+
if (p.isCancel(confirm) || !confirm) {
|
|
2026
|
+
p.log.info(dim("Cancelled."));
|
|
2027
|
+
return;
|
|
2028
|
+
}
|
|
2029
|
+
await writeFile(filePath, content, "utf-8");
|
|
2030
|
+
debug("setupCmintrcCommand: wrote %s", filePath);
|
|
2031
|
+
p.log.success(green(`Wrote ${fileName}`));
|
|
2032
|
+
}
|
|
2033
|
+
/** Project-local marker file that suppresses the preflight prompt forever. */
|
|
2034
|
+
const SKIP_SETUP_MARKER = ".cmint-skip-setup";
|
|
2035
|
+
/** True if at least one of biome/eslint/typescript/vitest is present. */
|
|
2036
|
+
function isAutoConfigurable(tools) {
|
|
2037
|
+
return Object.values(tools).some(Boolean);
|
|
2038
|
+
}
|
|
2039
|
+
/** True if the skip-setup marker exists in `cwd`. */
|
|
2040
|
+
async function hasSkipSetupMarker(cwd) {
|
|
2041
|
+
return exists(join(cwd, SKIP_SETUP_MARKER));
|
|
2042
|
+
}
|
|
2043
|
+
/** Write the skip-setup marker to `cwd`. The file is empty by design. */
|
|
2044
|
+
async function writeSkipSetupMarker(cwd) {
|
|
2045
|
+
const filePath = join(cwd, SKIP_SETUP_MARKER);
|
|
2046
|
+
await writeFile(filePath, "", "utf-8");
|
|
2047
|
+
debug("preflight: wrote skip-setup marker to %s", filePath);
|
|
2048
|
+
}
|
|
2049
|
+
/**
|
|
2050
|
+
* One-shot prompt run at the start of `cmint`. Skips silently if the user
|
|
2051
|
+
* already has a `.cmintrc` or has previously opted out (`.cmint-skip-setup`).
|
|
2052
|
+
* If the project is auto-configurable, asks the user whether to run setup
|
|
2053
|
+
* now. Choices: `yes` runs the standard setup flow; `no` proceeds without
|
|
2054
|
+
* setup and re-prompts next time; `never` writes a marker to suppress the
|
|
2055
|
+
* prompt for this project forever.
|
|
2056
|
+
*/
|
|
2057
|
+
async function runPreflightSetupPrompt(cwd) {
|
|
2058
|
+
debug("preflight: checking %s", cwd);
|
|
2059
|
+
if (await hasSkipSetupMarker(cwd)) {
|
|
2060
|
+
debug("preflight: skip-setup marker present, skipping prompt");
|
|
2061
|
+
return;
|
|
2062
|
+
}
|
|
2063
|
+
const existingConfig = await detectConfig(cwd);
|
|
2064
|
+
if (existingConfig) {
|
|
2065
|
+
debug("preflight: .cmintrc present at %s, skipping prompt", existingConfig);
|
|
2066
|
+
return;
|
|
2067
|
+
}
|
|
2068
|
+
if (!isAutoConfigurable(await detectTools(cwd))) {
|
|
2069
|
+
debug("preflight: project not auto-configurable, skipping prompt");
|
|
2070
|
+
return;
|
|
2071
|
+
}
|
|
2072
|
+
const choice = await p.select({
|
|
2073
|
+
message: "No .cmintrc found. Run setup to create one from detected tools?",
|
|
2074
|
+
options: [
|
|
2075
|
+
{
|
|
2076
|
+
label: "Yes, set up .cmintrc",
|
|
2077
|
+
value: "yes"
|
|
2078
|
+
},
|
|
2079
|
+
{
|
|
2080
|
+
label: "No, skip for now",
|
|
2081
|
+
value: "no"
|
|
2082
|
+
},
|
|
2083
|
+
{
|
|
2084
|
+
label: "No, don't ask again",
|
|
2085
|
+
value: "never"
|
|
2086
|
+
}
|
|
2087
|
+
]
|
|
2088
|
+
});
|
|
2089
|
+
if (p.isCancel(choice)) {
|
|
2090
|
+
debug("preflight: user cancelled prompt");
|
|
2091
|
+
return;
|
|
2092
|
+
}
|
|
2093
|
+
if (choice === "never") {
|
|
2094
|
+
await writeSkipSetupMarker(cwd);
|
|
2095
|
+
p.log.info(dim(`Won't ask again. Delete ${SKIP_SETUP_MARKER} to re-enable.`));
|
|
2096
|
+
return;
|
|
2097
|
+
}
|
|
2098
|
+
if (choice === "no") {
|
|
2099
|
+
p.log.info(dim("Skipping .cmintrc setup."));
|
|
2100
|
+
return;
|
|
2101
|
+
}
|
|
2102
|
+
debug("preflight: user chose yes, running setup");
|
|
2103
|
+
await setupCmintrcCommand(cwd);
|
|
2104
|
+
}
|
|
2105
|
+
//#endregion
|
|
1876
2106
|
//#region src/ui/staging-menu.ts
|
|
1877
2107
|
async function showStagingMenu(files, hasChecks) {
|
|
1878
2108
|
debug("showStagingMenu: %d files", files.length);
|
|
@@ -2058,6 +2288,8 @@ async function commitCommand(flags) {
|
|
|
2058
2288
|
debug("commitCommand called", { flags });
|
|
2059
2289
|
await assertGitRepo();
|
|
2060
2290
|
if (flags.retry) return handleRetry();
|
|
2291
|
+
const repoRoot = await getRepoRoot();
|
|
2292
|
+
await runPreflightSetupPrompt(repoRoot);
|
|
2061
2293
|
intro("🌿 commit-mint");
|
|
2062
2294
|
const status = await getStatusShort();
|
|
2063
2295
|
debug("Git status:", status || "(empty)");
|
|
@@ -2104,7 +2336,7 @@ async function commitCommand(flags) {
|
|
|
2104
2336
|
debug("All staged files are excluded:", diffResult.excludedFiles);
|
|
2105
2337
|
const message = buildExcludedFilesMessage(diffResult.excludedFiles);
|
|
2106
2338
|
log.info(diffResult.excludedFiles.map((f) => ` ${f}`).join("\n"));
|
|
2107
|
-
await saveCachedCommit(
|
|
2339
|
+
await saveCachedCommit(repoRoot, message);
|
|
2108
2340
|
s.start("Running pre-commit hooks...");
|
|
2109
2341
|
const result = await commitWithRecovery(message, s, await getHead());
|
|
2110
2342
|
if (result === "committed") {
|
|
@@ -2163,7 +2395,6 @@ async function commitCommand(flags) {
|
|
|
2163
2395
|
return;
|
|
2164
2396
|
}
|
|
2165
2397
|
message = reviewed;
|
|
2166
|
-
const repoRoot = await getRepoRoot();
|
|
2167
2398
|
await saveCachedCommit(repoRoot, message);
|
|
2168
2399
|
debug("Message cached for repo:", repoRoot);
|
|
2169
2400
|
s.start("Running pre-commit hooks...");
|
|
@@ -2347,13 +2578,20 @@ async function configCommand() {
|
|
|
2347
2578
|
p.note(buildConfigDisplay(config), "commit-mint config");
|
|
2348
2579
|
const action = await p.select({
|
|
2349
2580
|
message: "What would you like to do?",
|
|
2350
|
-
options: [
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2581
|
+
options: [
|
|
2582
|
+
{
|
|
2583
|
+
label: "Edit settings",
|
|
2584
|
+
value: "edit"
|
|
2585
|
+
},
|
|
2586
|
+
{
|
|
2587
|
+
label: "Setup .cmintrc",
|
|
2588
|
+
value: "setup"
|
|
2589
|
+
},
|
|
2590
|
+
{
|
|
2591
|
+
label: "Done",
|
|
2592
|
+
value: "done"
|
|
2593
|
+
}
|
|
2594
|
+
]
|
|
2357
2595
|
});
|
|
2358
2596
|
if (p.isCancel(action)) {
|
|
2359
2597
|
debug("configCommand: cancelled at main menu");
|
|
@@ -2365,6 +2603,11 @@ async function configCommand() {
|
|
|
2365
2603
|
p.outro("Config saved.");
|
|
2366
2604
|
return;
|
|
2367
2605
|
}
|
|
2606
|
+
if (action === "setup") {
|
|
2607
|
+
debug("configCommand: starting .cmintrc setup");
|
|
2608
|
+
await setupCmintrcCommand();
|
|
2609
|
+
continue;
|
|
2610
|
+
}
|
|
2368
2611
|
await editSettingsLoop(config);
|
|
2369
2612
|
}
|
|
2370
2613
|
}
|