@kyubiware/commit-mint 0.7.4 → 0.7.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 +160 -50
- package/dist/cli.mjs.map +1 -1
- package/package.json +1 -1
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.
|
|
32
|
+
version: "0.7.6",
|
|
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
|
|
1870
|
-
const {
|
|
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
|
-
|
|
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,8 @@ 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
|
|
2357
|
+
const FRESH_MS = 3600 * 1e3;
|
|
2358
|
+
const STALE_MS = 1440 * 60 * 1e3;
|
|
2315
2359
|
const FETCH_TIMEOUT_MS = 5e3;
|
|
2316
2360
|
let cachePath = join(os.homedir(), ".cache", "commit-mint", "update-check.json");
|
|
2317
2361
|
let fetchImpl = globalThis.fetch;
|
|
@@ -2399,6 +2443,70 @@ function displayNag(current, latest) {
|
|
|
2399
2443
|
log.warn(message);
|
|
2400
2444
|
}
|
|
2401
2445
|
/**
|
|
2446
|
+
* Fire-and-forget cache refresh used by the stale-while-revalidate (SWR) band
|
|
2447
|
+
* (cache age in [FRESH_MS, STALE_MS)). Runs the registry fetch and writes a
|
|
2448
|
+
* fresh cache entry without awaiting — the caller returns immediately with
|
|
2449
|
+
* the cached `latest` so the nag decision is fast.
|
|
2450
|
+
*
|
|
2451
|
+
* Wrapped in try/catch and `void`-ed so the floating promise NEVER rejects
|
|
2452
|
+
* (vitest fails the suite on unhandled rejections). On any failure (network,
|
|
2453
|
+
* HTTP non-ok, malformed JSON, fs write error) the cache file is left
|
|
2454
|
+
* unchanged and the failure is logged via {@link debug} only.
|
|
2455
|
+
*
|
|
2456
|
+
* No abort signal is wired here — the cancellable spinner only runs on the
|
|
2457
|
+
* STALE blocking-fetch path, and FRESH/SWR callers explicitly want this to
|
|
2458
|
+
* complete in the background regardless of user keystrokes.
|
|
2459
|
+
*/
|
|
2460
|
+
function refreshCacheInBackground() {
|
|
2461
|
+
debug("refreshCacheInBackground: starting fire-and-forget refresh");
|
|
2462
|
+
(async () => {
|
|
2463
|
+
try {
|
|
2464
|
+
const latest = await fetchLatest();
|
|
2465
|
+
if (latest === null) {
|
|
2466
|
+
debug("refreshCacheInBackground: fetch returned null, leaving cache unchanged");
|
|
2467
|
+
return;
|
|
2468
|
+
}
|
|
2469
|
+
await saveCache({
|
|
2470
|
+
latest,
|
|
2471
|
+
checkedAt: Date.now()
|
|
2472
|
+
});
|
|
2473
|
+
debug("refreshCacheInBackground: refreshed cache to latest=%s", latest);
|
|
2474
|
+
} catch (err) {
|
|
2475
|
+
debug("refreshCacheInBackground: failed — %s", err instanceof Error ? err.message : String(err));
|
|
2476
|
+
}
|
|
2477
|
+
})();
|
|
2478
|
+
}
|
|
2479
|
+
/**
|
|
2480
|
+
* Resolve the cache-hit branch (FRESH / SWR / stale-or-missing). Returns
|
|
2481
|
+
* `null` when the caller must fall through to the blocking-fetch path; returns
|
|
2482
|
+
* a {@link UpdateCheckStatus} when the cache hit is terminal.
|
|
2483
|
+
*
|
|
2484
|
+
* Side effect: kicks off a fire-and-forget {@link refreshCacheInBackground}
|
|
2485
|
+
* when age is in the SWR band [FRESH_MS, STALE_MS).
|
|
2486
|
+
*/
|
|
2487
|
+
function resolveCacheHit(cached, currentVersion, onNag) {
|
|
2488
|
+
if (cached === null) {
|
|
2489
|
+
debug("runUpdateCheck: no cache, fetching");
|
|
2490
|
+
return null;
|
|
2491
|
+
}
|
|
2492
|
+
const age = Date.now() - cached.checkedAt;
|
|
2493
|
+
if (age >= STALE_MS) {
|
|
2494
|
+
debug("runUpdateCheck: cache stale (>=%dh), refetching", STALE_MS / 36e5);
|
|
2495
|
+
return null;
|
|
2496
|
+
}
|
|
2497
|
+
if (age < FRESH_MS) debug("runUpdateCheck: cache fresh (<%dh), serving from cache", FRESH_MS / 36e5);
|
|
2498
|
+
else {
|
|
2499
|
+
debug("runUpdateCheck: cache in SWR band (<%dh), serving + background refresh", STALE_MS / 36e5);
|
|
2500
|
+
refreshCacheInBackground();
|
|
2501
|
+
}
|
|
2502
|
+
if (semver.gt(cached.latest, currentVersion)) {
|
|
2503
|
+
onNag(currentVersion, cached.latest);
|
|
2504
|
+
return "cache-update";
|
|
2505
|
+
}
|
|
2506
|
+
debug("runUpdateCheck: current >= latest, no nag");
|
|
2507
|
+
return "cache-current";
|
|
2508
|
+
}
|
|
2509
|
+
/**
|
|
2402
2510
|
* Run the full update check. Exported for tests; the public surface is
|
|
2403
2511
|
* {@link checkForUpdatesUpfront}. Accepts an optional AbortSignal that
|
|
2404
2512
|
* propagates to the underlying fetch — used by the cancellable spinner.
|
|
@@ -2419,18 +2527,8 @@ async function runUpdateCheck(currentVersion, parentSignal, onNag = displayNag)
|
|
|
2419
2527
|
return "skipped";
|
|
2420
2528
|
}
|
|
2421
2529
|
try {
|
|
2422
|
-
const
|
|
2423
|
-
if (
|
|
2424
|
-
debug("runUpdateCheck: cache fresh (<%dh), skipping fetch", TTL_MS / 36e5);
|
|
2425
|
-
if (semver.gt(cached.latest, currentVersion)) {
|
|
2426
|
-
onNag(currentVersion, cached.latest);
|
|
2427
|
-
return "cache-update";
|
|
2428
|
-
}
|
|
2429
|
-
debug("runUpdateCheck: current >= latest, no nag");
|
|
2430
|
-
return "cache-current";
|
|
2431
|
-
}
|
|
2432
|
-
if (cached) debug("runUpdateCheck: cache stale, refetching");
|
|
2433
|
-
else debug("runUpdateCheck: no cache, fetching");
|
|
2530
|
+
const cacheStatus = resolveCacheHit(await loadCache(), currentVersion, onNag);
|
|
2531
|
+
if (cacheStatus !== null) return cacheStatus;
|
|
2434
2532
|
const latest = await fetchLatest(parentSignal);
|
|
2435
2533
|
if (latest === null) {
|
|
2436
2534
|
debug("runUpdateCheck: fetch returned null, not saving cache");
|
|
@@ -2472,13 +2570,11 @@ async function checkForUpdatesUpfront(currentVersion) {
|
|
|
2472
2570
|
return;
|
|
2473
2571
|
}
|
|
2474
2572
|
const cached = await loadCache();
|
|
2475
|
-
if (cached && Date.now() - cached.checkedAt <
|
|
2476
|
-
|
|
2477
|
-
if (semver.gt(cached.latest, currentVersion)) displayNag(currentVersion, cached.latest);
|
|
2478
|
-
else debug("checkForUpdatesUpfront: current >= latest, no nag");
|
|
2573
|
+
if (cached && Date.now() - cached.checkedAt < STALE_MS) {
|
|
2574
|
+
await runUpdateCheck(currentVersion);
|
|
2479
2575
|
return;
|
|
2480
2576
|
}
|
|
2481
|
-
if (cached) debug("checkForUpdatesUpfront: cache stale, refetching");
|
|
2577
|
+
if (cached) debug("checkForUpdatesUpfront: cache stale (>=%dh), refetching", STALE_MS / 36e5);
|
|
2482
2578
|
else debug("checkForUpdatesUpfront: no cache, fetching");
|
|
2483
2579
|
const stdin = process.stdin;
|
|
2484
2580
|
if (stdin.isTTY !== true || typeof stdin.setRawMode !== "function") {
|
|
@@ -3009,11 +3105,13 @@ async function runPreCommitChecks(changedFiles, noCheck) {
|
|
|
3009
3105
|
}
|
|
3010
3106
|
/**
|
|
3011
3107
|
* Re-stage staged files whose working-tree content diverged from the index after checks ran.
|
|
3012
|
-
*
|
|
3108
|
+
* Signals (git status --short, 2-char XY code):
|
|
3109
|
+
* "MM" — tracked file staged-modified, then reformatted on disk
|
|
3110
|
+
* "AM" — newly-added file staged, then reformatted on disk
|
|
3013
3111
|
*/
|
|
3014
3112
|
async function restageFormatterModifications(stagedFileList) {
|
|
3015
3113
|
const checkedSet = new Set(stagedFileList);
|
|
3016
|
-
const modifiedByChecks = (await getChangedFiles()).filter((f) => checkedSet.has(f.path) && f.staged && f.status === "MM").map((f) => f.path);
|
|
3114
|
+
const modifiedByChecks = (await getChangedFiles()).filter((f) => checkedSet.has(f.path) && f.staged && (f.status === "MM" || f.status === "AM")).map((f) => f.path);
|
|
3017
3115
|
if (modifiedByChecks.length === 0) return;
|
|
3018
3116
|
debug("Re-staging %d file(s) modified by checks", modifiedByChecks.length);
|
|
3019
3117
|
await stageFiles(modifiedByChecks);
|
|
@@ -3127,7 +3225,19 @@ async function commitCommand(flags, version) {
|
|
|
3127
3225
|
}
|
|
3128
3226
|
s.stop("Message generated");
|
|
3129
3227
|
}
|
|
3130
|
-
const reviewed = await reviewCommitMessage(message)
|
|
3228
|
+
const reviewed = await reviewCommitMessage(message, { regenerate: async (hint) => {
|
|
3229
|
+
const combinedHint = flags.hint ? `${flags.hint}\n${hint}` : hint;
|
|
3230
|
+
debug("Regenerating with combined hint:", combinedHint);
|
|
3231
|
+
s.start("Regenerating commit message...");
|
|
3232
|
+
try {
|
|
3233
|
+
const newMessage = await generateMessage(diffResult.diff, combinedHint);
|
|
3234
|
+
s.stop("Message regenerated");
|
|
3235
|
+
return newMessage;
|
|
3236
|
+
} catch (err) {
|
|
3237
|
+
s.stop(red("Regeneration failed"));
|
|
3238
|
+
throw err;
|
|
3239
|
+
}
|
|
3240
|
+
} });
|
|
3131
3241
|
if (reviewed === null) {
|
|
3132
3242
|
outro(dim("Cancelled."));
|
|
3133
3243
|
return;
|