@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/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.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/services/clipboard.ts
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) {