@kyubiware/commit-mint 0.5.5 → 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 +266 -180
- 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" },
|
|
@@ -1294,6 +1294,188 @@ function validateGroups(groups, allFiles) {
|
|
|
1294
1294
|
return validated;
|
|
1295
1295
|
}
|
|
1296
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
|
|
1297
1479
|
//#region src/ui/grouping.ts
|
|
1298
1480
|
async function showGroupingConfirmation(groups, excluded) {
|
|
1299
1481
|
debug("showGroupingConfirmation: %d groups, %d excluded", groups.length, excluded.length);
|
|
@@ -1356,126 +1538,7 @@ function showGroupedFiles(groups, changedFiles) {
|
|
|
1356
1538
|
p.note(lines.join("\n"), "Commit groups");
|
|
1357
1539
|
}
|
|
1358
1540
|
//#endregion
|
|
1359
|
-
//#region src/
|
|
1360
|
-
async function copyToClipboard(content) {
|
|
1361
|
-
for (const [cmd, args] of [
|
|
1362
|
-
["wl-copy", []],
|
|
1363
|
-
["xclip", ["-selection", "clipboard"]],
|
|
1364
|
-
["xsel", ["--clipboard", "--input"]],
|
|
1365
|
-
["pbcopy", []]
|
|
1366
|
-
]) try {
|
|
1367
|
-
if (await new Promise((resolve) => {
|
|
1368
|
-
const child = spawn(cmd, args, { stdio: [
|
|
1369
|
-
"pipe",
|
|
1370
|
-
"ignore",
|
|
1371
|
-
"ignore"
|
|
1372
|
-
] });
|
|
1373
|
-
let settled = false;
|
|
1374
|
-
const done = (result) => {
|
|
1375
|
-
if (settled) return;
|
|
1376
|
-
settled = true;
|
|
1377
|
-
resolve(result);
|
|
1378
|
-
};
|
|
1379
|
-
child.on("error", () => done(false));
|
|
1380
|
-
child.on("exit", (code) => {
|
|
1381
|
-
if (code !== 0) done(false);
|
|
1382
|
-
});
|
|
1383
|
-
child.stdin.write(content, (err) => {
|
|
1384
|
-
if (err) {
|
|
1385
|
-
done(false);
|
|
1386
|
-
return;
|
|
1387
|
-
}
|
|
1388
|
-
child.stdin.end(() => {
|
|
1389
|
-
child.unref();
|
|
1390
|
-
done(true);
|
|
1391
|
-
});
|
|
1392
|
-
});
|
|
1393
|
-
})) return true;
|
|
1394
|
-
} catch {}
|
|
1395
|
-
return false;
|
|
1396
|
-
}
|
|
1397
|
-
//#endregion
|
|
1398
|
-
//#region src/ui/menu.ts
|
|
1399
|
-
async function showStagingMenu(files, hasChecks) {
|
|
1400
|
-
debug("showStagingMenu: %d files", files.length);
|
|
1401
|
-
const statusLabel = (status) => {
|
|
1402
|
-
switch (status) {
|
|
1403
|
-
case "M": return yellow("M");
|
|
1404
|
-
case "A": return green("A");
|
|
1405
|
-
case "D": return red("D");
|
|
1406
|
-
case "?":
|
|
1407
|
-
case "??": return cyan("?");
|
|
1408
|
-
default: return dim(status);
|
|
1409
|
-
}
|
|
1410
|
-
};
|
|
1411
|
-
const sorted = [...files].sort((a, b) => {
|
|
1412
|
-
if (a.staged !== b.staged) return a.staged ? -1 : 1;
|
|
1413
|
-
return a.path.localeCompare(b.path);
|
|
1414
|
-
});
|
|
1415
|
-
const stagedFiles = sorted.filter((f) => f.staged);
|
|
1416
|
-
const unstagedFiles = sorted.filter((f) => !f.staged);
|
|
1417
|
-
const lines = [];
|
|
1418
|
-
if (stagedFiles.length > 0) lines.push(green(bold("Staged:")), ...stagedFiles.map((f) => ` ${statusLabel(f.status)} ${f.path}`));
|
|
1419
|
-
if (unstagedFiles.length > 0) {
|
|
1420
|
-
if (lines.length > 0) lines.push("");
|
|
1421
|
-
lines.push(yellow(bold("Changed:")), ...unstagedFiles.map((f) => ` ${statusLabel(f.status)} ${f.path}`));
|
|
1422
|
-
}
|
|
1423
|
-
p.note(lines.join("\n"), `${files.length} file${files.length !== 1 ? "s" : ""}`);
|
|
1424
|
-
const choice = await p.select({
|
|
1425
|
-
message: "Stage files for commit:",
|
|
1426
|
-
options: [
|
|
1427
|
-
{
|
|
1428
|
-
label: "Auto-group into commits",
|
|
1429
|
-
value: "autogroup",
|
|
1430
|
-
hint: "LLM groups files into logical commits"
|
|
1431
|
-
},
|
|
1432
|
-
...stagedFiles.length > 0 ? [{
|
|
1433
|
-
label: "Commit staged files only",
|
|
1434
|
-
value: "staged",
|
|
1435
|
-
hint: `${stagedFiles.length} file${stagedFiles.length !== 1 ? "s" : ""} already staged`
|
|
1436
|
-
}] : [],
|
|
1437
|
-
{
|
|
1438
|
-
label: "Stage all files",
|
|
1439
|
-
value: "all",
|
|
1440
|
-
hint: `${files.length} file${files.length !== 1 ? "s" : ""}`
|
|
1441
|
-
},
|
|
1442
|
-
...hasChecks ? [{
|
|
1443
|
-
label: "Run checks",
|
|
1444
|
-
value: "checks",
|
|
1445
|
-
hint: "Pre-flight checks from cmint config"
|
|
1446
|
-
}] : [],
|
|
1447
|
-
{
|
|
1448
|
-
label: "Select files...",
|
|
1449
|
-
value: "select"
|
|
1450
|
-
},
|
|
1451
|
-
{
|
|
1452
|
-
label: "Cancel",
|
|
1453
|
-
value: "cancel"
|
|
1454
|
-
}
|
|
1455
|
-
]
|
|
1456
|
-
});
|
|
1457
|
-
if (p.isCancel(choice) || choice === "cancel") return null;
|
|
1458
|
-
if (choice === "autogroup") return "autogroup";
|
|
1459
|
-
if (choice === "checks") return "checks";
|
|
1460
|
-
if (choice === "staged") return "staged";
|
|
1461
|
-
if (choice === "all") return {
|
|
1462
|
-
files: files.map((f) => f.path),
|
|
1463
|
-
all: true
|
|
1464
|
-
};
|
|
1465
|
-
const selected = await p.multiselect({
|
|
1466
|
-
message: "Select files to stage:",
|
|
1467
|
-
options: sorted.map((f) => ({
|
|
1468
|
-
label: `${statusLabel(f.status)} ${f.path}`,
|
|
1469
|
-
value: f.path
|
|
1470
|
-
})),
|
|
1471
|
-
required: true
|
|
1472
|
-
});
|
|
1473
|
-
if (p.isCancel(selected)) return null;
|
|
1474
|
-
return {
|
|
1475
|
-
files: selected,
|
|
1476
|
-
all: false
|
|
1477
|
-
};
|
|
1478
|
-
}
|
|
1541
|
+
//#region src/ui/recovery-menu.ts
|
|
1479
1542
|
async function showRecoveryMenu(errors, onRetry, onSkipHooks, onRestage, message, rawStderr) {
|
|
1480
1543
|
debug("showRecoveryMenu: %d errors", errors.length);
|
|
1481
1544
|
let clipboardCopied = false;
|
|
@@ -1577,65 +1640,6 @@ async function showRecoveryMenu(errors, onRetry, onSkipHooks, onRestage, message
|
|
|
1577
1640
|
}
|
|
1578
1641
|
}
|
|
1579
1642
|
}
|
|
1580
|
-
async function showCheckFailureMenu(errors, rawStderr, onRetry) {
|
|
1581
|
-
debug("showCheckFailureMenu: %d errors", errors.length);
|
|
1582
|
-
let clipboardCopied = false;
|
|
1583
|
-
p.note(errors.map((e) => ` ${red("•")} [${e.tool}] ${e.message}`).join("\n"), red("Pre-commit check failed"));
|
|
1584
|
-
while (true) {
|
|
1585
|
-
const choice = await p.select({
|
|
1586
|
-
message: "What do you want to do?",
|
|
1587
|
-
options: [
|
|
1588
|
-
{
|
|
1589
|
-
label: clipboardCopied ? `${green("✓")} Copy error report to clipboard` : "Copy error report to clipboard",
|
|
1590
|
-
value: "copy"
|
|
1591
|
-
},
|
|
1592
|
-
{
|
|
1593
|
-
label: "View full error output",
|
|
1594
|
-
value: "view",
|
|
1595
|
-
hint: "Show the raw stderr from checks"
|
|
1596
|
-
},
|
|
1597
|
-
{
|
|
1598
|
-
label: "Retry checks",
|
|
1599
|
-
value: "retry",
|
|
1600
|
-
hint: "Re-run checks after fixing errors"
|
|
1601
|
-
},
|
|
1602
|
-
{
|
|
1603
|
-
label: "Skip checks and commit",
|
|
1604
|
-
value: "skip"
|
|
1605
|
-
},
|
|
1606
|
-
{
|
|
1607
|
-
label: "Cancel",
|
|
1608
|
-
value: "cancel"
|
|
1609
|
-
}
|
|
1610
|
-
]
|
|
1611
|
-
});
|
|
1612
|
-
if (p.isCancel(choice)) {
|
|
1613
|
-
debug("showCheckFailureMenu: user cancelled");
|
|
1614
|
-
return "cancelled";
|
|
1615
|
-
}
|
|
1616
|
-
debug("showCheckFailureMenu: user chose %s", choice);
|
|
1617
|
-
switch (choice) {
|
|
1618
|
-
case "copy":
|
|
1619
|
-
if (await copyToClipboard(rawStderr)) {
|
|
1620
|
-
clipboardCopied = true;
|
|
1621
|
-
p.log.step(green("Copied to clipboard."));
|
|
1622
|
-
} else p.log.warn(red("No clipboard tool found. Install xclip, wl-copy, or xsel."));
|
|
1623
|
-
continue;
|
|
1624
|
-
case "view":
|
|
1625
|
-
p.note(rawStderr.trim() || "(no raw output)", "Full error output");
|
|
1626
|
-
continue;
|
|
1627
|
-
case "retry":
|
|
1628
|
-
if (onRetry) return "retried";
|
|
1629
|
-
return "retried";
|
|
1630
|
-
case "skip":
|
|
1631
|
-
p.log.info("Skipping checks and proceeding with commit...");
|
|
1632
|
-
return "skipped";
|
|
1633
|
-
case "cancel":
|
|
1634
|
-
p.outro(dim("Cancelled."));
|
|
1635
|
-
return "cancelled";
|
|
1636
|
-
}
|
|
1637
|
-
}
|
|
1638
|
-
}
|
|
1639
1643
|
//#endregion
|
|
1640
1644
|
//#region src/commands/auto-group.ts
|
|
1641
1645
|
async function runAutoGroupFlow(changedFiles, flags) {
|
|
@@ -1869,6 +1873,88 @@ async function handleRetry() {
|
|
|
1869
1873
|
else process.exit(1);
|
|
1870
1874
|
}
|
|
1871
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
|
|
1872
1958
|
//#region src/commands/staging.ts
|
|
1873
1959
|
/** Interactive staging loop for multiple changed files */
|
|
1874
1960
|
async function handleStaging(changedFiles, flags) {
|