@kyubiware/commit-mint 0.5.4 → 0.5.6
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 +85 -154
- package/dist/cli.mjs +297 -191
- package/dist/cli.mjs.map +1 -1
- package/package.json +1 -1
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.5.
|
|
31
|
+
version: "0.5.6",
|
|
32
32
|
description: "🌿 A commit tool that actually handles hook failures",
|
|
33
33
|
type: "module",
|
|
34
34
|
bin: { "cmint": "./dist/cli.mjs" },
|
|
@@ -1031,23 +1031,44 @@ function buildCommand(command, files) {
|
|
|
1031
1031
|
return `${command} ${files.map((f) => f.includes(" ") ? `"${f}"` : f).join(" ")}`;
|
|
1032
1032
|
}
|
|
1033
1033
|
/**
|
|
1034
|
-
*
|
|
1035
|
-
|
|
1034
|
+
* Call a function command with matched files and normalize the result to ResolvedCommand[].
|
|
1035
|
+
*/
|
|
1036
|
+
function resolveFunction(fn, matchedFiles) {
|
|
1037
|
+
const resolved = fn(matchedFiles);
|
|
1038
|
+
return (Array.isArray(resolved) ? resolved : [resolved]).map((command) => ({
|
|
1039
|
+
command,
|
|
1040
|
+
fromFunction: true
|
|
1041
|
+
}));
|
|
1042
|
+
}
|
|
1043
|
+
/**
|
|
1044
|
+
* Resolve config commands for a glob entry into an array of resolved commands.
|
|
1045
|
+
* Function commands are called with matched filenames; string commands are kept as-is.
|
|
1046
|
+
* Each resolved entry tracks whether it came from a function (for file-append behavior).
|
|
1036
1047
|
*/
|
|
1037
1048
|
function resolveCommands(commands, matchedFiles) {
|
|
1038
|
-
if (typeof commands === "function")
|
|
1039
|
-
|
|
1040
|
-
|
|
1049
|
+
if (typeof commands === "function") return resolveFunction(commands, matchedFiles);
|
|
1050
|
+
if (Array.isArray(commands)) {
|
|
1051
|
+
const result = [];
|
|
1052
|
+
for (const cmd of commands) if (typeof cmd === "function") result.push(...resolveFunction(cmd, matchedFiles));
|
|
1053
|
+
else result.push({
|
|
1054
|
+
command: cmd,
|
|
1055
|
+
fromFunction: false
|
|
1056
|
+
});
|
|
1057
|
+
return result;
|
|
1041
1058
|
}
|
|
1042
|
-
return
|
|
1059
|
+
return [{
|
|
1060
|
+
command: commands,
|
|
1061
|
+
fromFunction: false
|
|
1062
|
+
}];
|
|
1043
1063
|
}
|
|
1044
1064
|
/**
|
|
1045
1065
|
* Run resolved commands for a single glob entry, appending results.
|
|
1066
|
+
* Function-originated commands run as-is; string commands get matched files appended.
|
|
1046
1067
|
* Returns false if any command fails (for fail-fast signaling).
|
|
1047
1068
|
*/
|
|
1048
|
-
async function runCommandsForGlob(cmds,
|
|
1049
|
-
for (const
|
|
1050
|
-
const fullCommand =
|
|
1069
|
+
async function runCommandsForGlob(cmds, matchedFiles, timeout, results, repoRoot) {
|
|
1070
|
+
for (const { command, fromFunction } of cmds) {
|
|
1071
|
+
const fullCommand = fromFunction ? command : buildCommand(command, matchedFiles);
|
|
1051
1072
|
debug("runCommandsForGlob: running '%s'", fullCommand);
|
|
1052
1073
|
const result = await runCommand(fullCommand, timeout, repoRoot);
|
|
1053
1074
|
results.push({
|
|
@@ -1080,13 +1101,12 @@ async function runAllChecks(repoRoot, stagedFiles, timeout) {
|
|
|
1080
1101
|
const results = [];
|
|
1081
1102
|
for (const [glob, commands] of Object.entries(config)) {
|
|
1082
1103
|
const matchedFiles = matchFiles(glob, stagedFiles);
|
|
1083
|
-
const isFunction = typeof commands === "function";
|
|
1084
1104
|
if (matchedFiles.length === 0) {
|
|
1085
1105
|
debug("runAllChecks: no files matched pattern '%s'", glob);
|
|
1086
1106
|
continue;
|
|
1087
1107
|
}
|
|
1088
1108
|
debug("runAllChecks: pattern '%s' matched %d files", glob, matchedFiles.length);
|
|
1089
|
-
if (!await runCommandsForGlob(resolveCommands(commands, matchedFiles),
|
|
1109
|
+
if (!await runCommandsForGlob(resolveCommands(commands, matchedFiles), matchedFiles, timeout, results, repoRoot)) return {
|
|
1090
1110
|
ok: false,
|
|
1091
1111
|
results
|
|
1092
1112
|
};
|
|
@@ -1274,6 +1294,188 @@ function validateGroups(groups, allFiles) {
|
|
|
1274
1294
|
return validated;
|
|
1275
1295
|
}
|
|
1276
1296
|
//#endregion
|
|
1297
|
+
//#region src/services/clipboard.ts
|
|
1298
|
+
/** Milliseconds to wait after stdin closes for quick exit failures. */
|
|
1299
|
+
const GRACE_PERIOD_MS = 150;
|
|
1300
|
+
async function copyToClipboard(content) {
|
|
1301
|
+
for (const [cmd, args] of [
|
|
1302
|
+
["wl-copy", []],
|
|
1303
|
+
["xclip", ["-selection", "clipboard"]],
|
|
1304
|
+
["xsel", ["--clipboard", "--input"]],
|
|
1305
|
+
["pbcopy", []]
|
|
1306
|
+
]) try {
|
|
1307
|
+
if (await tryCopy(cmd, args, content)) return true;
|
|
1308
|
+
} catch {}
|
|
1309
|
+
return false;
|
|
1310
|
+
}
|
|
1311
|
+
/**
|
|
1312
|
+
* Try to copy content using a single clipboard tool.
|
|
1313
|
+
*
|
|
1314
|
+
* Waits a short grace period after stdin closes to detect quick failures
|
|
1315
|
+
* (e.g. wl-copy on non-Wayland, missing display). If the tool survives
|
|
1316
|
+
* the grace period, assumes success — clipboard tools like xclip and
|
|
1317
|
+
* wl-copy hold the selection open indefinitely, so we can't wait for exit.
|
|
1318
|
+
*/
|
|
1319
|
+
/**
|
|
1320
|
+
* Evaluate clipboard tool status after the grace period.
|
|
1321
|
+
* If the child already exited, report based on exit code.
|
|
1322
|
+
* If still alive, assume success (clipboard tools hold selection open).
|
|
1323
|
+
*/
|
|
1324
|
+
function handleGracePeriod(settled, exitCode, stderrChunks, child, done) {
|
|
1325
|
+
if (settled) return;
|
|
1326
|
+
if (exitCode !== null) {
|
|
1327
|
+
if (exitCode === 0) done(true, "exited 0");
|
|
1328
|
+
else {
|
|
1329
|
+
const stderr = Buffer.concat(stderrChunks).toString().trim();
|
|
1330
|
+
done(false, `exit ${exitCode}${stderr ? `: ${stderr}` : ""}`);
|
|
1331
|
+
}
|
|
1332
|
+
return;
|
|
1333
|
+
}
|
|
1334
|
+
child.unref();
|
|
1335
|
+
done(true);
|
|
1336
|
+
}
|
|
1337
|
+
function tryCopy(cmd, args, content) {
|
|
1338
|
+
return new Promise((resolve) => {
|
|
1339
|
+
debug("clipboard: trying %s", cmd);
|
|
1340
|
+
const child = spawn(cmd, args, { stdio: [
|
|
1341
|
+
"pipe",
|
|
1342
|
+
"ignore",
|
|
1343
|
+
"pipe"
|
|
1344
|
+
] });
|
|
1345
|
+
let settled = false;
|
|
1346
|
+
const stderrChunks = [];
|
|
1347
|
+
child.stderr?.on("data", (chunk) => {
|
|
1348
|
+
stderrChunks.push(chunk);
|
|
1349
|
+
});
|
|
1350
|
+
const done = (result, reason) => {
|
|
1351
|
+
if (settled) return;
|
|
1352
|
+
settled = true;
|
|
1353
|
+
debug("clipboard: %s %s%s", cmd, result ? "ok" : "failed", reason ? ` (${reason})` : "");
|
|
1354
|
+
resolve(result);
|
|
1355
|
+
};
|
|
1356
|
+
child.on("error", (err) => {
|
|
1357
|
+
done(false, err.message);
|
|
1358
|
+
});
|
|
1359
|
+
let exitCode = null;
|
|
1360
|
+
child.on("exit", (code) => {
|
|
1361
|
+
exitCode = code;
|
|
1362
|
+
});
|
|
1363
|
+
child.stdin.write(content, (err) => {
|
|
1364
|
+
if (err) {
|
|
1365
|
+
done(false, "stdin write error");
|
|
1366
|
+
return;
|
|
1367
|
+
}
|
|
1368
|
+
child.stdin.end(() => {
|
|
1369
|
+
setTimeout(() => handleGracePeriod(settled, exitCode, stderrChunks, child, done), GRACE_PERIOD_MS);
|
|
1370
|
+
});
|
|
1371
|
+
});
|
|
1372
|
+
});
|
|
1373
|
+
}
|
|
1374
|
+
//#endregion
|
|
1375
|
+
//#region src/ui/check-failure-menu.ts
|
|
1376
|
+
const MAX_TSC_DIAGNOSTICS = 3;
|
|
1377
|
+
const MAX_SUMMARY_LINE_LENGTH = 120;
|
|
1378
|
+
const TSC_DIAGNOSTIC = /^(.+?\.(?:ts|tsx|mts|cts|js|jsx|mjs|cjs))\((\d+),(\d+)\):\s+error\s+(TS\d+):\s+(.+)$/;
|
|
1379
|
+
function formatCheckFailureSummary(errors) {
|
|
1380
|
+
if (errors.length === 0) return "No check error details were parsed. View full output for details.";
|
|
1381
|
+
return errors.map((error) => formatCheckErrorSummary(error)).join("\n");
|
|
1382
|
+
}
|
|
1383
|
+
function formatCheckErrorSummary(error) {
|
|
1384
|
+
if (error.tool === "tsc") {
|
|
1385
|
+
const diagnostics = extractTscDiagnostics(error.raw || error.message);
|
|
1386
|
+
if (diagnostics.length > 0) return formatTscSummary(diagnostics);
|
|
1387
|
+
}
|
|
1388
|
+
const message = firstMeaningfulLine(error.message || error.raw);
|
|
1389
|
+
return ` ${red("•")} [${error.tool}] ${truncate(message, MAX_SUMMARY_LINE_LENGTH)}`;
|
|
1390
|
+
}
|
|
1391
|
+
function extractTscDiagnostics(raw) {
|
|
1392
|
+
return raw.split("\n").map((line) => line.trim()).map((line) => {
|
|
1393
|
+
const match = TSC_DIAGNOSTIC.exec(line);
|
|
1394
|
+
if (!match) return null;
|
|
1395
|
+
return {
|
|
1396
|
+
file: match[1] ?? "",
|
|
1397
|
+
line: match[2] ?? "",
|
|
1398
|
+
column: match[3] ?? "",
|
|
1399
|
+
code: match[4] ?? "",
|
|
1400
|
+
message: match[5] ?? ""
|
|
1401
|
+
};
|
|
1402
|
+
}).filter((diagnostic) => diagnostic !== null);
|
|
1403
|
+
}
|
|
1404
|
+
function formatTscSummary(diagnostics) {
|
|
1405
|
+
const visible = diagnostics.slice(0, MAX_TSC_DIAGNOSTICS);
|
|
1406
|
+
const hidden = diagnostics.length - visible.length;
|
|
1407
|
+
const lines = [` ${red("•")} [tsc] ${diagnostics.length} TypeScript error${diagnostics.length !== 1 ? "s" : ""}`, ...visible.map((diagnostic) => `${diagnostic.file}:${diagnostic.line}:${diagnostic.column} — error ${diagnostic.code}: ${truncate(diagnostic.message, MAX_SUMMARY_LINE_LENGTH)}`)];
|
|
1408
|
+
if (hidden > 0) lines.push(dim(` +${hidden} more TypeScript error${hidden !== 1 ? "s" : ""}. View full output for details.`));
|
|
1409
|
+
return lines.join("\n");
|
|
1410
|
+
}
|
|
1411
|
+
function firstMeaningfulLine(message) {
|
|
1412
|
+
return message.split("\n").map((l) => l.trim()).find((l) => l.length > 0 && !l.startsWith(">") && !l.startsWith("ELIFECYCLE")) ?? message;
|
|
1413
|
+
}
|
|
1414
|
+
function truncate(message, maxLength) {
|
|
1415
|
+
const collapsed = message.replace(/\s+/g, " ").trim();
|
|
1416
|
+
if (collapsed.length <= maxLength) return collapsed;
|
|
1417
|
+
return `${collapsed.slice(0, Math.max(0, maxLength - 1))}…`;
|
|
1418
|
+
}
|
|
1419
|
+
async function showCheckFailureMenu(errors, rawStderr, onRetry) {
|
|
1420
|
+
debug("showCheckFailureMenu: %d errors", errors.length);
|
|
1421
|
+
let clipboardCopied = false;
|
|
1422
|
+
p.note(formatCheckFailureSummary(errors), red("Pre-commit check failed"));
|
|
1423
|
+
while (true) {
|
|
1424
|
+
const choice = await p.select({
|
|
1425
|
+
message: "What do you want to do?",
|
|
1426
|
+
options: [
|
|
1427
|
+
{
|
|
1428
|
+
label: clipboardCopied ? `${green("✓")} Copy error report to clipboard` : "Copy error report to clipboard",
|
|
1429
|
+
value: "copy"
|
|
1430
|
+
},
|
|
1431
|
+
{
|
|
1432
|
+
label: "View full error output",
|
|
1433
|
+
value: "view",
|
|
1434
|
+
hint: "Show the raw stderr from checks"
|
|
1435
|
+
},
|
|
1436
|
+
{
|
|
1437
|
+
label: "Retry checks",
|
|
1438
|
+
value: "retry",
|
|
1439
|
+
hint: "Re-run checks after fixing errors"
|
|
1440
|
+
},
|
|
1441
|
+
{
|
|
1442
|
+
label: "Skip checks and commit",
|
|
1443
|
+
value: "skip"
|
|
1444
|
+
},
|
|
1445
|
+
{
|
|
1446
|
+
label: "Cancel",
|
|
1447
|
+
value: "cancel"
|
|
1448
|
+
}
|
|
1449
|
+
]
|
|
1450
|
+
});
|
|
1451
|
+
if (p.isCancel(choice)) {
|
|
1452
|
+
debug("showCheckFailureMenu: user cancelled");
|
|
1453
|
+
return "cancelled";
|
|
1454
|
+
}
|
|
1455
|
+
debug("showCheckFailureMenu: user chose %s", choice);
|
|
1456
|
+
switch (choice) {
|
|
1457
|
+
case "copy":
|
|
1458
|
+
if (await copyToClipboard(rawStderr)) {
|
|
1459
|
+
clipboardCopied = true;
|
|
1460
|
+
p.log.step(green("Copied to clipboard."));
|
|
1461
|
+
} else p.log.warn(red("No clipboard tool found. Install xclip, wl-copy, or xsel."));
|
|
1462
|
+
continue;
|
|
1463
|
+
case "view":
|
|
1464
|
+
p.note(rawStderr.trim() || "(no raw output)", "Full error output");
|
|
1465
|
+
continue;
|
|
1466
|
+
case "retry":
|
|
1467
|
+
if (onRetry) return "retried";
|
|
1468
|
+
return "retried";
|
|
1469
|
+
case "skip":
|
|
1470
|
+
p.log.info("Skipping checks and proceeding with commit...");
|
|
1471
|
+
return "skipped";
|
|
1472
|
+
case "cancel":
|
|
1473
|
+
p.outro(dim("Cancelled."));
|
|
1474
|
+
return "cancelled";
|
|
1475
|
+
}
|
|
1476
|
+
}
|
|
1477
|
+
}
|
|
1478
|
+
//#endregion
|
|
1277
1479
|
//#region src/ui/grouping.ts
|
|
1278
1480
|
async function showGroupingConfirmation(groups, excluded) {
|
|
1279
1481
|
debug("showGroupingConfirmation: %d groups, %d excluded", groups.length, excluded.length);
|
|
@@ -1336,126 +1538,7 @@ function showGroupedFiles(groups, changedFiles) {
|
|
|
1336
1538
|
p.note(lines.join("\n"), "Commit groups");
|
|
1337
1539
|
}
|
|
1338
1540
|
//#endregion
|
|
1339
|
-
//#region src/
|
|
1340
|
-
async function copyToClipboard(content) {
|
|
1341
|
-
for (const [cmd, args] of [
|
|
1342
|
-
["wl-copy", []],
|
|
1343
|
-
["xclip", ["-selection", "clipboard"]],
|
|
1344
|
-
["xsel", ["--clipboard", "--input"]],
|
|
1345
|
-
["pbcopy", []]
|
|
1346
|
-
]) try {
|
|
1347
|
-
if (await new Promise((resolve) => {
|
|
1348
|
-
const child = spawn(cmd, args, { stdio: [
|
|
1349
|
-
"pipe",
|
|
1350
|
-
"ignore",
|
|
1351
|
-
"ignore"
|
|
1352
|
-
] });
|
|
1353
|
-
let settled = false;
|
|
1354
|
-
const done = (result) => {
|
|
1355
|
-
if (settled) return;
|
|
1356
|
-
settled = true;
|
|
1357
|
-
resolve(result);
|
|
1358
|
-
};
|
|
1359
|
-
child.on("error", () => done(false));
|
|
1360
|
-
child.on("exit", (code) => {
|
|
1361
|
-
if (code !== 0) done(false);
|
|
1362
|
-
});
|
|
1363
|
-
child.stdin.write(content, (err) => {
|
|
1364
|
-
if (err) {
|
|
1365
|
-
done(false);
|
|
1366
|
-
return;
|
|
1367
|
-
}
|
|
1368
|
-
child.stdin.end(() => {
|
|
1369
|
-
child.unref();
|
|
1370
|
-
done(true);
|
|
1371
|
-
});
|
|
1372
|
-
});
|
|
1373
|
-
})) return true;
|
|
1374
|
-
} catch {}
|
|
1375
|
-
return false;
|
|
1376
|
-
}
|
|
1377
|
-
//#endregion
|
|
1378
|
-
//#region src/ui/menu.ts
|
|
1379
|
-
async function showStagingMenu(files, hasChecks) {
|
|
1380
|
-
debug("showStagingMenu: %d files", files.length);
|
|
1381
|
-
const statusLabel = (status) => {
|
|
1382
|
-
switch (status) {
|
|
1383
|
-
case "M": return yellow("M");
|
|
1384
|
-
case "A": return green("A");
|
|
1385
|
-
case "D": return red("D");
|
|
1386
|
-
case "?":
|
|
1387
|
-
case "??": return cyan("?");
|
|
1388
|
-
default: return dim(status);
|
|
1389
|
-
}
|
|
1390
|
-
};
|
|
1391
|
-
const sorted = [...files].sort((a, b) => {
|
|
1392
|
-
if (a.staged !== b.staged) return a.staged ? -1 : 1;
|
|
1393
|
-
return a.path.localeCompare(b.path);
|
|
1394
|
-
});
|
|
1395
|
-
const stagedFiles = sorted.filter((f) => f.staged);
|
|
1396
|
-
const unstagedFiles = sorted.filter((f) => !f.staged);
|
|
1397
|
-
const lines = [];
|
|
1398
|
-
if (stagedFiles.length > 0) lines.push(green(bold("Staged:")), ...stagedFiles.map((f) => ` ${statusLabel(f.status)} ${f.path}`));
|
|
1399
|
-
if (unstagedFiles.length > 0) {
|
|
1400
|
-
if (lines.length > 0) lines.push("");
|
|
1401
|
-
lines.push(yellow(bold("Changed:")), ...unstagedFiles.map((f) => ` ${statusLabel(f.status)} ${f.path}`));
|
|
1402
|
-
}
|
|
1403
|
-
p.note(lines.join("\n"), `${files.length} file${files.length !== 1 ? "s" : ""}`);
|
|
1404
|
-
const choice = await p.select({
|
|
1405
|
-
message: "Stage files for commit:",
|
|
1406
|
-
options: [
|
|
1407
|
-
{
|
|
1408
|
-
label: "Auto-group into commits",
|
|
1409
|
-
value: "autogroup",
|
|
1410
|
-
hint: "LLM groups files into logical commits"
|
|
1411
|
-
},
|
|
1412
|
-
...stagedFiles.length > 0 ? [{
|
|
1413
|
-
label: "Commit staged files only",
|
|
1414
|
-
value: "staged",
|
|
1415
|
-
hint: `${stagedFiles.length} file${stagedFiles.length !== 1 ? "s" : ""} already staged`
|
|
1416
|
-
}] : [],
|
|
1417
|
-
{
|
|
1418
|
-
label: "Stage all files",
|
|
1419
|
-
value: "all",
|
|
1420
|
-
hint: `${files.length} file${files.length !== 1 ? "s" : ""}`
|
|
1421
|
-
},
|
|
1422
|
-
...hasChecks ? [{
|
|
1423
|
-
label: "Run checks",
|
|
1424
|
-
value: "checks",
|
|
1425
|
-
hint: "Pre-flight checks from cmint config"
|
|
1426
|
-
}] : [],
|
|
1427
|
-
{
|
|
1428
|
-
label: "Select files...",
|
|
1429
|
-
value: "select"
|
|
1430
|
-
},
|
|
1431
|
-
{
|
|
1432
|
-
label: "Cancel",
|
|
1433
|
-
value: "cancel"
|
|
1434
|
-
}
|
|
1435
|
-
]
|
|
1436
|
-
});
|
|
1437
|
-
if (p.isCancel(choice) || choice === "cancel") return null;
|
|
1438
|
-
if (choice === "autogroup") return "autogroup";
|
|
1439
|
-
if (choice === "checks") return "checks";
|
|
1440
|
-
if (choice === "staged") return "staged";
|
|
1441
|
-
if (choice === "all") return {
|
|
1442
|
-
files: files.map((f) => f.path),
|
|
1443
|
-
all: true
|
|
1444
|
-
};
|
|
1445
|
-
const selected = await p.multiselect({
|
|
1446
|
-
message: "Select files to stage:",
|
|
1447
|
-
options: sorted.map((f) => ({
|
|
1448
|
-
label: `${statusLabel(f.status)} ${f.path}`,
|
|
1449
|
-
value: f.path
|
|
1450
|
-
})),
|
|
1451
|
-
required: true
|
|
1452
|
-
});
|
|
1453
|
-
if (p.isCancel(selected)) return null;
|
|
1454
|
-
return {
|
|
1455
|
-
files: selected,
|
|
1456
|
-
all: false
|
|
1457
|
-
};
|
|
1458
|
-
}
|
|
1541
|
+
//#region src/ui/recovery-menu.ts
|
|
1459
1542
|
async function showRecoveryMenu(errors, onRetry, onSkipHooks, onRestage, message, rawStderr) {
|
|
1460
1543
|
debug("showRecoveryMenu: %d errors", errors.length);
|
|
1461
1544
|
let clipboardCopied = false;
|
|
@@ -1557,65 +1640,6 @@ async function showRecoveryMenu(errors, onRetry, onSkipHooks, onRestage, message
|
|
|
1557
1640
|
}
|
|
1558
1641
|
}
|
|
1559
1642
|
}
|
|
1560
|
-
async function showCheckFailureMenu(errors, rawStderr, onRetry) {
|
|
1561
|
-
debug("showCheckFailureMenu: %d errors", errors.length);
|
|
1562
|
-
let clipboardCopied = false;
|
|
1563
|
-
p.note(errors.map((e) => ` ${red("•")} [${e.tool}] ${e.message}`).join("\n"), red("Pre-commit check failed"));
|
|
1564
|
-
while (true) {
|
|
1565
|
-
const choice = await p.select({
|
|
1566
|
-
message: "What do you want to do?",
|
|
1567
|
-
options: [
|
|
1568
|
-
{
|
|
1569
|
-
label: clipboardCopied ? `${green("✓")} Copy error report to clipboard` : "Copy error report to clipboard",
|
|
1570
|
-
value: "copy"
|
|
1571
|
-
},
|
|
1572
|
-
{
|
|
1573
|
-
label: "View full error output",
|
|
1574
|
-
value: "view",
|
|
1575
|
-
hint: "Show the raw stderr from checks"
|
|
1576
|
-
},
|
|
1577
|
-
{
|
|
1578
|
-
label: "Retry checks",
|
|
1579
|
-
value: "retry",
|
|
1580
|
-
hint: "Re-run checks after fixing errors"
|
|
1581
|
-
},
|
|
1582
|
-
{
|
|
1583
|
-
label: "Skip checks and commit",
|
|
1584
|
-
value: "skip"
|
|
1585
|
-
},
|
|
1586
|
-
{
|
|
1587
|
-
label: "Cancel",
|
|
1588
|
-
value: "cancel"
|
|
1589
|
-
}
|
|
1590
|
-
]
|
|
1591
|
-
});
|
|
1592
|
-
if (p.isCancel(choice)) {
|
|
1593
|
-
debug("showCheckFailureMenu: user cancelled");
|
|
1594
|
-
return "cancelled";
|
|
1595
|
-
}
|
|
1596
|
-
debug("showCheckFailureMenu: user chose %s", choice);
|
|
1597
|
-
switch (choice) {
|
|
1598
|
-
case "copy":
|
|
1599
|
-
if (await copyToClipboard(rawStderr)) {
|
|
1600
|
-
clipboardCopied = true;
|
|
1601
|
-
p.log.step(green("Copied to clipboard."));
|
|
1602
|
-
} else p.log.warn(red("No clipboard tool found. Install xclip, wl-copy, or xsel."));
|
|
1603
|
-
continue;
|
|
1604
|
-
case "view":
|
|
1605
|
-
p.note(rawStderr.trim() || "(no raw output)", "Full error output");
|
|
1606
|
-
continue;
|
|
1607
|
-
case "retry":
|
|
1608
|
-
if (onRetry) return "retried";
|
|
1609
|
-
return "retried";
|
|
1610
|
-
case "skip":
|
|
1611
|
-
p.log.info("Skipping checks and proceeding with commit...");
|
|
1612
|
-
return "skipped";
|
|
1613
|
-
case "cancel":
|
|
1614
|
-
p.outro(dim("Cancelled."));
|
|
1615
|
-
return "cancelled";
|
|
1616
|
-
}
|
|
1617
|
-
}
|
|
1618
|
-
}
|
|
1619
1643
|
//#endregion
|
|
1620
1644
|
//#region src/commands/auto-group.ts
|
|
1621
1645
|
async function runAutoGroupFlow(changedFiles, flags) {
|
|
@@ -1849,6 +1873,88 @@ async function handleRetry() {
|
|
|
1849
1873
|
else process.exit(1);
|
|
1850
1874
|
}
|
|
1851
1875
|
//#endregion
|
|
1876
|
+
//#region src/ui/staging-menu.ts
|
|
1877
|
+
async function showStagingMenu(files, hasChecks) {
|
|
1878
|
+
debug("showStagingMenu: %d files", files.length);
|
|
1879
|
+
const statusLabel = (status) => {
|
|
1880
|
+
switch (status) {
|
|
1881
|
+
case "M": return yellow("M");
|
|
1882
|
+
case "A": return green("A");
|
|
1883
|
+
case "D": return red("D");
|
|
1884
|
+
case "?":
|
|
1885
|
+
case "??": return cyan("?");
|
|
1886
|
+
default: return dim(status);
|
|
1887
|
+
}
|
|
1888
|
+
};
|
|
1889
|
+
const sorted = [...files].sort((a, b) => {
|
|
1890
|
+
if (a.staged !== b.staged) return a.staged ? -1 : 1;
|
|
1891
|
+
return a.path.localeCompare(b.path);
|
|
1892
|
+
});
|
|
1893
|
+
const stagedFiles = sorted.filter((f) => f.staged);
|
|
1894
|
+
const unstagedFiles = sorted.filter((f) => !f.staged);
|
|
1895
|
+
const lines = [];
|
|
1896
|
+
if (stagedFiles.length > 0) lines.push(green(bold("Staged:")), ...stagedFiles.map((f) => ` ${statusLabel(f.status)} ${f.path}`));
|
|
1897
|
+
if (unstagedFiles.length > 0) {
|
|
1898
|
+
if (lines.length > 0) lines.push("");
|
|
1899
|
+
lines.push(yellow(bold("Changed:")), ...unstagedFiles.map((f) => ` ${statusLabel(f.status)} ${f.path}`));
|
|
1900
|
+
}
|
|
1901
|
+
p.note(lines.join("\n"), `${files.length} file${files.length !== 1 ? "s" : ""}`);
|
|
1902
|
+
const choice = await p.select({
|
|
1903
|
+
message: "Stage files for commit:",
|
|
1904
|
+
options: [
|
|
1905
|
+
{
|
|
1906
|
+
label: "Auto-group into commits",
|
|
1907
|
+
value: "autogroup",
|
|
1908
|
+
hint: "LLM groups files into logical commits"
|
|
1909
|
+
},
|
|
1910
|
+
...stagedFiles.length > 0 ? [{
|
|
1911
|
+
label: "Commit staged files only",
|
|
1912
|
+
value: "staged",
|
|
1913
|
+
hint: `${stagedFiles.length} file${stagedFiles.length !== 1 ? "s" : ""} already staged`
|
|
1914
|
+
}] : [],
|
|
1915
|
+
{
|
|
1916
|
+
label: "Stage all files",
|
|
1917
|
+
value: "all",
|
|
1918
|
+
hint: `${files.length} file${files.length !== 1 ? "s" : ""}`
|
|
1919
|
+
},
|
|
1920
|
+
...hasChecks ? [{
|
|
1921
|
+
label: "Run checks",
|
|
1922
|
+
value: "checks",
|
|
1923
|
+
hint: "Pre-flight checks from cmint config"
|
|
1924
|
+
}] : [],
|
|
1925
|
+
{
|
|
1926
|
+
label: "Select files...",
|
|
1927
|
+
value: "select"
|
|
1928
|
+
},
|
|
1929
|
+
{
|
|
1930
|
+
label: "Cancel",
|
|
1931
|
+
value: "cancel"
|
|
1932
|
+
}
|
|
1933
|
+
]
|
|
1934
|
+
});
|
|
1935
|
+
if (p.isCancel(choice) || choice === "cancel") return null;
|
|
1936
|
+
if (choice === "autogroup") return "autogroup";
|
|
1937
|
+
if (choice === "checks") return "checks";
|
|
1938
|
+
if (choice === "staged") return "staged";
|
|
1939
|
+
if (choice === "all") return {
|
|
1940
|
+
files: files.map((f) => f.path),
|
|
1941
|
+
all: true
|
|
1942
|
+
};
|
|
1943
|
+
const selected = await p.multiselect({
|
|
1944
|
+
message: "Select files to stage:",
|
|
1945
|
+
options: sorted.map((f) => ({
|
|
1946
|
+
label: `${statusLabel(f.status)} ${f.path}`,
|
|
1947
|
+
value: f.path
|
|
1948
|
+
})),
|
|
1949
|
+
required: true
|
|
1950
|
+
});
|
|
1951
|
+
if (p.isCancel(selected)) return null;
|
|
1952
|
+
return {
|
|
1953
|
+
files: selected,
|
|
1954
|
+
all: false
|
|
1955
|
+
};
|
|
1956
|
+
}
|
|
1957
|
+
//#endregion
|
|
1852
1958
|
//#region src/commands/staging.ts
|
|
1853
1959
|
/** Interactive staging loop for multiple changed files */
|
|
1854
1960
|
async function handleStaging(changedFiles, flags) {
|