@kyubiware/commit-mint 0.7.3 → 0.7.5

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
@@ -29,7 +29,7 @@ var __exportAll = (all, no_symbols) => {
29
29
  //#region package.json
30
30
  var package_default = {
31
31
  name: "@kyubiware/commit-mint",
32
- version: "0.7.3",
32
+ version: "0.7.5",
33
33
  description: "🌿 AI-powered git commit tool — auto-group changed files, generate messages, run pre-commit checks",
34
34
  type: "module",
35
35
  bin: { "cmint": "./dist/cli.mjs" },
@@ -1866,25 +1866,65 @@ async function showRecoveryMenu(errors, onRetry, onSkipHooks, onRestage, message
1866
1866
  }
1867
1867
  //#endregion
1868
1868
  //#region src/ui/review-message.ts
1869
- async function reviewCommitMessage(message) {
1870
- const { select, text } = await import("@clack/prompts");
1869
+ async function handleEdit(message) {
1870
+ const { text } = await import("@clack/prompts");
1871
+ const edited = await text({
1872
+ message: "Edit commit message:",
1873
+ initialValue: message,
1874
+ validate: (v) => v?.trim() ? void 0 : "Message cannot be empty"
1875
+ });
1876
+ if (isCancel(edited)) {
1877
+ debug("User cancelled edit, returning to review menu");
1878
+ return null;
1879
+ }
1880
+ const newMessage = String(edited).trim();
1881
+ debug("Edited message:", newMessage);
1882
+ return newMessage;
1883
+ }
1884
+ async function handleRegenerate(regenerate) {
1885
+ const { log, text } = await import("@clack/prompts");
1886
+ const hint = await text({
1887
+ message: "Describe what this commit is about to guide regeneration:",
1888
+ validate: (v) => v?.trim() ? void 0 : "Hint cannot be empty"
1889
+ });
1890
+ if (isCancel(hint)) {
1891
+ debug("User cancelled hint entry, returning to review menu");
1892
+ return null;
1893
+ }
1894
+ const hintValue = String(hint).trim();
1895
+ debug("Regenerating with hint:", hintValue);
1896
+ try {
1897
+ const newMessage = await regenerate(hintValue);
1898
+ debug("Regenerated message:", newMessage);
1899
+ return newMessage;
1900
+ } catch (err) {
1901
+ const errMsg = err instanceof Error ? err.message : String(err);
1902
+ debug("Regeneration failed:", errMsg);
1903
+ log.warn(red(`Regeneration failed: ${errMsg}`));
1904
+ return null;
1905
+ }
1906
+ }
1907
+ async function reviewCommitMessage(message, options) {
1908
+ const { select } = await import("@clack/prompts");
1871
1909
  while (true) {
1910
+ const reviewOptions = [{
1911
+ label: "Use as-is",
1912
+ value: "use"
1913
+ }, {
1914
+ label: "Edit",
1915
+ value: "edit"
1916
+ }];
1917
+ if (options?.regenerate) reviewOptions.push({
1918
+ label: "Regenerate with hint",
1919
+ value: "regenerate"
1920
+ });
1921
+ reviewOptions.push({
1922
+ label: "Cancel",
1923
+ value: "cancel"
1924
+ });
1872
1925
  const review = await select({
1873
1926
  message: `Review commit message:\n\n ${bold(message)}\n`,
1874
- options: [
1875
- {
1876
- label: "Use as-is",
1877
- value: "use"
1878
- },
1879
- {
1880
- label: "Edit",
1881
- value: "edit"
1882
- },
1883
- {
1884
- label: "Cancel",
1885
- value: "cancel"
1886
- }
1887
- ]
1927
+ options: reviewOptions
1888
1928
  });
1889
1929
  if (isCancel(review) || review === "cancel") {
1890
1930
  debug("User cancelled at review step");
@@ -1894,17 +1934,8 @@ async function reviewCommitMessage(message) {
1894
1934
  debug("User accepted message");
1895
1935
  return message;
1896
1936
  }
1897
- if (review === "edit") {
1898
- debug("User chose to edit message");
1899
- const edited = await text({
1900
- message: "Edit commit message:",
1901
- initialValue: message,
1902
- validate: (v) => v?.trim() ? void 0 : "Message cannot be empty"
1903
- });
1904
- if (isCancel(edited)) continue;
1905
- message = String(edited).trim();
1906
- debug("Edited message:", message);
1907
- }
1937
+ if (review === "edit") message = await handleEdit(message) ?? message;
1938
+ if (review === "regenerate" && options?.regenerate) message = await handleRegenerate(options.regenerate) ?? message;
1908
1939
  }
1909
1940
  }
1910
1941
  //#endregion
@@ -2019,7 +2050,19 @@ async function runAutoGroupFlow(changedFiles, flags) {
2019
2050
  log.info(dim(message));
2020
2051
  if (flags.auto) debug("Auto mode: accepting generated message");
2021
2052
  else {
2022
- const reviewed = await reviewCommitMessage(message);
2053
+ const reviewed = await reviewCommitMessage(message, { regenerate: async (hint) => {
2054
+ const combinedHint = flags.hint ? `${flags.hint}\n${hint}` : hint;
2055
+ debug("Regenerating with combined hint:", combinedHint);
2056
+ s.start("Regenerating commit message...");
2057
+ try {
2058
+ const newMessage = await generateMessage(diffResult.diff, combinedHint);
2059
+ s.stop("Message regenerated");
2060
+ return newMessage;
2061
+ } catch (err) {
2062
+ s.stop(red("Regeneration failed"));
2063
+ throw err;
2064
+ }
2065
+ } });
2023
2066
  if (reviewed === null) {
2024
2067
  outro(dim("Cancelled."));
2025
2068
  return "cancelled";
@@ -2311,7 +2354,6 @@ async function agentCommand(flags) {
2311
2354
  //#endregion
2312
2355
  //#region src/services/update-check.ts
2313
2356
  const REGISTRY_URL = "https://registry.npmjs.org/-/package/@kyubiware/commit-mint/dist-tags";
2314
- const PACKAGE_NAME$1 = "@kyubiware/commit-mint";
2315
2357
  const TTL_MS = 1440 * 60 * 1e3;
2316
2358
  const FETCH_TIMEOUT_MS = 5e3;
2317
2359
  let cachePath = join(os.homedir(), ".cache", "commit-mint", "update-check.json");
@@ -2396,7 +2438,7 @@ async function fetchLatest(parentSignal) {
2396
2438
  }
2397
2439
  function displayNag(current, latest) {
2398
2440
  debug("displayNag: %s → %s", current, latest);
2399
- const message = `Update available: ${yellow(current)} → ${green(latest)}\nRun ${cyan(`npm update -g ${PACKAGE_NAME$1}`)} to update`;
2441
+ const message = `Update available: ${yellow(current)} → ${green(latest)}\nRun ${cyan("cmint update")} to update`;
2400
2442
  log.warn(message);
2401
2443
  }
2402
2444
  /**
@@ -2404,11 +2446,16 @@ function displayNag(current, latest) {
2404
2446
  * {@link checkForUpdatesUpfront}. Accepts an optional AbortSignal that
2405
2447
  * propagates to the underlying fetch — used by the cancellable spinner.
2406
2448
  *
2449
+ * When an update is available, {@link onNag} is invoked (defaulting to
2450
+ * {@link displayNag}). The cancellable spinner path passes a capturing
2451
+ * callback so it can stop the spinner BEFORE the nag prints — otherwise
2452
+ * `log.warn` interleaves with the spinner line and leaves it on screen.
2453
+ *
2407
2454
  * Returns an {@link UpdateCheckStatus} so the caller can distinguish a
2408
2455
  * real fetch that found the user current (eligible for "You are on the
2409
2456
  * latest version" feedback) from a silent cache hit.
2410
2457
  */
2411
- async function runUpdateCheck(currentVersion, parentSignal) {
2458
+ async function runUpdateCheck(currentVersion, parentSignal, onNag = displayNag) {
2412
2459
  debug("runUpdateCheck: currentVersion=%s", currentVersion);
2413
2460
  if (shouldSkip(currentVersion)) {
2414
2461
  debug("runUpdateCheck: skipped (NO_UPDATE_NOTIFIER / CI / NODE_ENV=test / non-TTY / invalid version)");
@@ -2419,7 +2466,7 @@ async function runUpdateCheck(currentVersion, parentSignal) {
2419
2466
  if (cached && Date.now() - cached.checkedAt < TTL_MS) {
2420
2467
  debug("runUpdateCheck: cache fresh (<%dh), skipping fetch", TTL_MS / 36e5);
2421
2468
  if (semver.gt(cached.latest, currentVersion)) {
2422
- displayNag(currentVersion, cached.latest);
2469
+ onNag(currentVersion, cached.latest);
2423
2470
  return "cache-update";
2424
2471
  }
2425
2472
  debug("runUpdateCheck: current >= latest, no nag");
@@ -2437,7 +2484,7 @@ async function runUpdateCheck(currentVersion, parentSignal) {
2437
2484
  checkedAt: Date.now()
2438
2485
  });
2439
2486
  if (semver.gt(latest, currentVersion)) {
2440
- displayNag(currentVersion, latest);
2487
+ onNag(currentVersion, latest);
2441
2488
  return "fetch-update";
2442
2489
  }
2443
2490
  debug("runUpdateCheck: current >= latest, no nag");
@@ -2560,9 +2607,15 @@ async function runCheckWithSpinner(currentVersion) {
2560
2607
  reportFetchCurrent(await runUpdateCheck(currentVersion));
2561
2608
  return;
2562
2609
  }
2610
+ const captured = { nag: null };
2563
2611
  let status;
2564
2612
  try {
2565
- status = await runUpdateCheck(currentVersion, controller.signal);
2613
+ status = await runUpdateCheck(currentVersion, controller.signal, (current, latest) => {
2614
+ captured.nag = {
2615
+ current,
2616
+ latest
2617
+ };
2618
+ });
2566
2619
  } finally {
2567
2620
  handler.cleanup();
2568
2621
  }
@@ -2570,8 +2623,10 @@ async function runCheckWithSpinner(currentVersion) {
2570
2623
  debug("runCheckWithSpinner: spinner dismissed by user");
2571
2624
  s.stop("Skipped");
2572
2625
  } else if (status === "fetch-current") s.stop(green("You are on the latest version"));
2573
- else if (status === "fetch-update") s.stop("");
2574
- else if (status === "fetch-failed-or-aborted") s.stop("Update check failed");
2626
+ else if (status === "fetch-update" || status === "cache-update") {
2627
+ s.stop("");
2628
+ if (captured.nag) displayNag(captured.nag.current, captured.nag.latest);
2629
+ } else if (status === "fetch-failed-or-aborted") s.stop("Update check failed");
2575
2630
  else s.stop("");
2576
2631
  }
2577
2632
  //#endregion
@@ -2997,11 +3052,13 @@ async function runPreCommitChecks(changedFiles, noCheck) {
2997
3052
  }
2998
3053
  /**
2999
3054
  * Re-stage staged files whose working-tree content diverged from the index after checks ran.
3000
- * Signal: a file with both index and working-tree modifications has git status "MM".
3055
+ * Signals (git status --short, 2-char XY code):
3056
+ * "MM" — tracked file staged-modified, then reformatted on disk
3057
+ * "AM" — newly-added file staged, then reformatted on disk
3001
3058
  */
3002
3059
  async function restageFormatterModifications(stagedFileList) {
3003
3060
  const checkedSet = new Set(stagedFileList);
3004
- const modifiedByChecks = (await getChangedFiles()).filter((f) => checkedSet.has(f.path) && f.staged && f.status === "MM").map((f) => f.path);
3061
+ const modifiedByChecks = (await getChangedFiles()).filter((f) => checkedSet.has(f.path) && f.staged && (f.status === "MM" || f.status === "AM")).map((f) => f.path);
3005
3062
  if (modifiedByChecks.length === 0) return;
3006
3063
  debug("Re-staging %d file(s) modified by checks", modifiedByChecks.length);
3007
3064
  await stageFiles(modifiedByChecks);
@@ -3115,7 +3172,19 @@ async function commitCommand(flags, version) {
3115
3172
  }
3116
3173
  s.stop("Message generated");
3117
3174
  }
3118
- const reviewed = await reviewCommitMessage(message);
3175
+ const reviewed = await reviewCommitMessage(message, { regenerate: async (hint) => {
3176
+ const combinedHint = flags.hint ? `${flags.hint}\n${hint}` : hint;
3177
+ debug("Regenerating with combined hint:", combinedHint);
3178
+ s.start("Regenerating commit message...");
3179
+ try {
3180
+ const newMessage = await generateMessage(diffResult.diff, combinedHint);
3181
+ s.stop("Message regenerated");
3182
+ return newMessage;
3183
+ } catch (err) {
3184
+ s.stop(red("Regeneration failed"));
3185
+ throw err;
3186
+ }
3187
+ } });
3119
3188
  if (reviewed === null) {
3120
3189
  outro(dim("Cancelled."));
3121
3190
  return;