@kyubiware/commit-mint 0.7.0 → 0.7.3
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 +13 -0
- package/dist/cli.mjs +468 -89
- package/dist/cli.mjs.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -31,10 +31,23 @@ API key. The key is saved to `~/.commit-mint` (INI).
|
|
|
31
31
|
cmint # interactive: stage → checks → review → commit
|
|
32
32
|
cmint -a # auto-group, generate messages, commit everything
|
|
33
33
|
cmint config # edit provider, model, locale, etc.
|
|
34
|
+
cmint update # update cmint to the latest published version
|
|
34
35
|
```
|
|
35
36
|
|
|
36
37
|
See [all flags](#all-flags) for the full list.
|
|
37
38
|
|
|
39
|
+
## Self-update (`cmint update`)
|
|
40
|
+
|
|
41
|
+
`cmint update` checks the npm registry for a newer version and, if one exists,
|
|
42
|
+
runs the appropriate global install command for your package manager (detected
|
|
43
|
+
from `npm_config_user_agent` — npm, pnpm, yarn, or bun; falls back to npm).
|
|
44
|
+
|
|
45
|
+
- If you're already on the latest version, it exits silently.
|
|
46
|
+
- Otherwise it prints `current → latest` and asks for confirmation before
|
|
47
|
+
running the install (live npm output is streamed to your terminal).
|
|
48
|
+
- `cmint update -y` (or `--yes`) skips the confirmation prompt — useful in
|
|
49
|
+
scripts.
|
|
50
|
+
|
|
38
51
|
## Pre-flight checks (`.cmintrc`)
|
|
39
52
|
|
|
40
53
|
`.cmintrc` is commit-mint's pre-commit check system. The config syntax is
|
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.3",
|
|
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" },
|
|
@@ -1497,9 +1497,11 @@ function tryCopy(cmd, args, content) {
|
|
|
1497
1497
|
//#region src/ui/check-failure-menu.ts
|
|
1498
1498
|
const MAX_TSC_DIAGNOSTICS = 3;
|
|
1499
1499
|
const MAX_ESLINT_DIAGNOSTICS = 3;
|
|
1500
|
+
const MAX_TEST_FAILURES = 3;
|
|
1500
1501
|
const MAX_SUMMARY_LINE_LENGTH = 120;
|
|
1501
1502
|
const TSC_DIAGNOSTIC = /^(.+?\.(?:ts|tsx|mts|cts|js|jsx|mjs|cjs))\((\d+),(\d+)\):\s+error\s+(TS\d+):\s+(.+)$/;
|
|
1502
1503
|
const ESLINT_ERROR_LINE = /^\s*(\d+):(\d+)\s+(error|warning)\s+(.+)\s{2,}(\S+)\s*$/;
|
|
1504
|
+
const TEST_FILE_FAIL = /^\s*FAIL\s+(.+?\.(?:test|spec)\.[^\s>]+)\s*>\s*(.+)$/;
|
|
1503
1505
|
function formatCheckFailureSummary(errors) {
|
|
1504
1506
|
if (errors.length === 0) return "No check error details were parsed. View full output for details.";
|
|
1505
1507
|
return errors.map((error) => formatCheckErrorSummary(error)).join("\n");
|
|
@@ -1513,6 +1515,10 @@ function formatCheckErrorSummary(error) {
|
|
|
1513
1515
|
const diagnostics = extractEslintDiagnostics(error.raw || error.message);
|
|
1514
1516
|
if (diagnostics.length > 0) return formatEslintSummary(diagnostics);
|
|
1515
1517
|
}
|
|
1518
|
+
if (error.tool === "vitest" || error.tool === "jest") {
|
|
1519
|
+
const failures = extractTestFailures(error.raw || error.message);
|
|
1520
|
+
if (failures.length > 0) return formatTestFailureSummary(failures, error.tool);
|
|
1521
|
+
}
|
|
1516
1522
|
const message = firstMeaningfulLine(error.message || error.raw);
|
|
1517
1523
|
return ` ${red("•")} [${error.tool}] ${truncate(message, MAX_SUMMARY_LINE_LENGTH)}`;
|
|
1518
1524
|
}
|
|
@@ -1557,6 +1563,46 @@ function extractEslintDiagnostics(raw) {
|
|
|
1557
1563
|
}
|
|
1558
1564
|
return diagnostics;
|
|
1559
1565
|
}
|
|
1566
|
+
function extractTestFailures(raw) {
|
|
1567
|
+
const failures = [];
|
|
1568
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1569
|
+
for (const line of raw.split("\n")) {
|
|
1570
|
+
const match = TEST_FILE_FAIL.exec(line);
|
|
1571
|
+
if (!match) continue;
|
|
1572
|
+
const file = (match[1] ?? "").trim();
|
|
1573
|
+
const name = (match[2] ?? "").trim();
|
|
1574
|
+
if (!file || !name) continue;
|
|
1575
|
+
const key = `${file}\u0000${name}`;
|
|
1576
|
+
if (seen.has(key)) continue;
|
|
1577
|
+
seen.add(key);
|
|
1578
|
+
failures.push({
|
|
1579
|
+
file,
|
|
1580
|
+
name
|
|
1581
|
+
});
|
|
1582
|
+
}
|
|
1583
|
+
return failures;
|
|
1584
|
+
}
|
|
1585
|
+
function formatTestFailureSummary(failures, tool) {
|
|
1586
|
+
const total = failures.length;
|
|
1587
|
+
const visible = failures.slice(0, MAX_TEST_FAILURES);
|
|
1588
|
+
const hidden = total - visible.length;
|
|
1589
|
+
const fileCount = new Set(failures.map((f) => f.file)).size;
|
|
1590
|
+
const testNoun = total === 1 ? "test" : "tests";
|
|
1591
|
+
const fileNoun = fileCount === 1 ? "file" : "files";
|
|
1592
|
+
const lines = [` ${red("•")} [${tool}] ${total} failed ${testNoun} in ${fileCount} ${fileNoun}`];
|
|
1593
|
+
const byFile = /* @__PURE__ */ new Map();
|
|
1594
|
+
for (const failure of visible) {
|
|
1595
|
+
const names = byFile.get(failure.file) ?? [];
|
|
1596
|
+
names.push(failure.name);
|
|
1597
|
+
byFile.set(failure.file, names);
|
|
1598
|
+
}
|
|
1599
|
+
for (const [file, names] of byFile) {
|
|
1600
|
+
lines.push(` ${truncate(file, MAX_SUMMARY_LINE_LENGTH)}`);
|
|
1601
|
+
for (const name of names) lines.push(` ${red("×")} ${truncate(name, MAX_SUMMARY_LINE_LENGTH)}`);
|
|
1602
|
+
}
|
|
1603
|
+
if (hidden > 0) lines.push(dim(` +${hidden} more failed ${hidden === 1 ? "test" : "tests"}. View full output for details.`));
|
|
1604
|
+
return lines.join("\n");
|
|
1605
|
+
}
|
|
1560
1606
|
function formatEslintSummary(diagnostics) {
|
|
1561
1607
|
const visible = diagnostics.slice(0, MAX_ESLINT_DIAGNOSTICS);
|
|
1562
1608
|
const hidden = diagnostics.length - visible.length;
|
|
@@ -2263,6 +2309,272 @@ async function agentCommand(flags) {
|
|
|
2263
2309
|
});
|
|
2264
2310
|
}
|
|
2265
2311
|
//#endregion
|
|
2312
|
+
//#region src/services/update-check.ts
|
|
2313
|
+
const REGISTRY_URL = "https://registry.npmjs.org/-/package/@kyubiware/commit-mint/dist-tags";
|
|
2314
|
+
const PACKAGE_NAME$1 = "@kyubiware/commit-mint";
|
|
2315
|
+
const TTL_MS = 1440 * 60 * 1e3;
|
|
2316
|
+
const FETCH_TIMEOUT_MS = 5e3;
|
|
2317
|
+
let cachePath = join(os.homedir(), ".cache", "commit-mint", "update-check.json");
|
|
2318
|
+
let fetchImpl = globalThis.fetch;
|
|
2319
|
+
/**
|
|
2320
|
+
* Returns true when the notifier must not run: env opt-out, CI, test env,
|
|
2321
|
+
* non-TTY stderr, or an invalid/missing current version.
|
|
2322
|
+
*/
|
|
2323
|
+
function shouldSkip(currentVersion) {
|
|
2324
|
+
const noUpdate = process.env.NO_UPDATE_NOTIFIER;
|
|
2325
|
+
if (noUpdate !== void 0 && noUpdate !== "") return true;
|
|
2326
|
+
const ci = process.env.CI;
|
|
2327
|
+
if (ci !== void 0 && ci !== "" && ci !== "0") return true;
|
|
2328
|
+
if (process.env.NODE_ENV === "test") return true;
|
|
2329
|
+
if (process.stderr.isTTY !== true) return true;
|
|
2330
|
+
if (currentVersion === void 0) return true;
|
|
2331
|
+
if (currentVersion === "") return true;
|
|
2332
|
+
if (semver.valid(currentVersion) === null) return true;
|
|
2333
|
+
return false;
|
|
2334
|
+
}
|
|
2335
|
+
function loadCache() {
|
|
2336
|
+
try {
|
|
2337
|
+
const raw = readFileSync(cachePath, "utf8");
|
|
2338
|
+
const parsed = JSON.parse(raw);
|
|
2339
|
+
if (parsed !== null && typeof parsed === "object" && typeof parsed.latest === "string" && typeof parsed.checkedAt === "number") {
|
|
2340
|
+
const entry = parsed;
|
|
2341
|
+
const ageH = ((Date.now() - entry.checkedAt) / 36e5).toFixed(2);
|
|
2342
|
+
debug("loadCache: hit — latest=%s, age=%sh", entry.latest, ageH);
|
|
2343
|
+
return Promise.resolve(entry);
|
|
2344
|
+
}
|
|
2345
|
+
debug("loadCache: miss — malformed entry");
|
|
2346
|
+
return Promise.resolve(null);
|
|
2347
|
+
} catch {
|
|
2348
|
+
debug("loadCache: miss — %s does not exist", cachePath);
|
|
2349
|
+
return Promise.resolve(null);
|
|
2350
|
+
}
|
|
2351
|
+
}
|
|
2352
|
+
function saveCache(entry) {
|
|
2353
|
+
try {
|
|
2354
|
+
mkdirSync(dirname(cachePath), { recursive: true });
|
|
2355
|
+
writeFileSync(cachePath, JSON.stringify(entry), "utf8");
|
|
2356
|
+
debug("saveCache: wrote latest=%s to %s", entry.latest, cachePath);
|
|
2357
|
+
} catch (err) {
|
|
2358
|
+
debug("saveCache: failed — %s", err instanceof Error ? err.message : String(err));
|
|
2359
|
+
}
|
|
2360
|
+
return Promise.resolve();
|
|
2361
|
+
}
|
|
2362
|
+
/**
|
|
2363
|
+
* Fetch latest version from the registry. Aborts on FETCH_TIMEOUT_MS or when
|
|
2364
|
+
* `parentSignal` aborts (user keypress). Returns null on any failure so the
|
|
2365
|
+
* caller can degrade silently.
|
|
2366
|
+
*/
|
|
2367
|
+
async function fetchLatest(parentSignal) {
|
|
2368
|
+
debug("fetchLatest: GET %s (timeout=%dms)", REGISTRY_URL, FETCH_TIMEOUT_MS);
|
|
2369
|
+
try {
|
|
2370
|
+
const controller = new AbortController();
|
|
2371
|
+
const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
|
|
2372
|
+
const onParentAbort = () => controller.abort();
|
|
2373
|
+
if (parentSignal) if (parentSignal.aborted) controller.abort();
|
|
2374
|
+
else parentSignal.addEventListener("abort", onParentAbort, { once: true });
|
|
2375
|
+
try {
|
|
2376
|
+
const response = await fetchImpl(REGISTRY_URL, { signal: controller.signal });
|
|
2377
|
+
if (!response.ok) {
|
|
2378
|
+
debug("fetchLatest: HTTP %d — returning null", response.status);
|
|
2379
|
+
return null;
|
|
2380
|
+
}
|
|
2381
|
+
const data = await response.json();
|
|
2382
|
+
if (typeof data.latest !== "string") {
|
|
2383
|
+
debug("fetchLatest: response missing 'latest' field — returning null");
|
|
2384
|
+
return null;
|
|
2385
|
+
}
|
|
2386
|
+
debug("fetchLatest: ok — latest=%s", data.latest);
|
|
2387
|
+
return data.latest;
|
|
2388
|
+
} finally {
|
|
2389
|
+
clearTimeout(timer);
|
|
2390
|
+
if (parentSignal) parentSignal.removeEventListener("abort", onParentAbort);
|
|
2391
|
+
}
|
|
2392
|
+
} catch (err) {
|
|
2393
|
+
debug("fetchLatest: error — %s", err instanceof Error ? err.message : String(err));
|
|
2394
|
+
return null;
|
|
2395
|
+
}
|
|
2396
|
+
}
|
|
2397
|
+
function displayNag(current, latest) {
|
|
2398
|
+
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`;
|
|
2400
|
+
log.warn(message);
|
|
2401
|
+
}
|
|
2402
|
+
/**
|
|
2403
|
+
* Run the full update check. Exported for tests; the public surface is
|
|
2404
|
+
* {@link checkForUpdatesUpfront}. Accepts an optional AbortSignal that
|
|
2405
|
+
* propagates to the underlying fetch — used by the cancellable spinner.
|
|
2406
|
+
*
|
|
2407
|
+
* Returns an {@link UpdateCheckStatus} so the caller can distinguish a
|
|
2408
|
+
* real fetch that found the user current (eligible for "You are on the
|
|
2409
|
+
* latest version" feedback) from a silent cache hit.
|
|
2410
|
+
*/
|
|
2411
|
+
async function runUpdateCheck(currentVersion, parentSignal) {
|
|
2412
|
+
debug("runUpdateCheck: currentVersion=%s", currentVersion);
|
|
2413
|
+
if (shouldSkip(currentVersion)) {
|
|
2414
|
+
debug("runUpdateCheck: skipped (NO_UPDATE_NOTIFIER / CI / NODE_ENV=test / non-TTY / invalid version)");
|
|
2415
|
+
return "skipped";
|
|
2416
|
+
}
|
|
2417
|
+
try {
|
|
2418
|
+
const cached = await loadCache();
|
|
2419
|
+
if (cached && Date.now() - cached.checkedAt < TTL_MS) {
|
|
2420
|
+
debug("runUpdateCheck: cache fresh (<%dh), skipping fetch", TTL_MS / 36e5);
|
|
2421
|
+
if (semver.gt(cached.latest, currentVersion)) {
|
|
2422
|
+
displayNag(currentVersion, cached.latest);
|
|
2423
|
+
return "cache-update";
|
|
2424
|
+
}
|
|
2425
|
+
debug("runUpdateCheck: current >= latest, no nag");
|
|
2426
|
+
return "cache-current";
|
|
2427
|
+
}
|
|
2428
|
+
if (cached) debug("runUpdateCheck: cache stale, refetching");
|
|
2429
|
+
else debug("runUpdateCheck: no cache, fetching");
|
|
2430
|
+
const latest = await fetchLatest(parentSignal);
|
|
2431
|
+
if (latest === null) {
|
|
2432
|
+
debug("runUpdateCheck: fetch returned null, not saving cache");
|
|
2433
|
+
return "fetch-failed-or-aborted";
|
|
2434
|
+
}
|
|
2435
|
+
await saveCache({
|
|
2436
|
+
latest,
|
|
2437
|
+
checkedAt: Date.now()
|
|
2438
|
+
});
|
|
2439
|
+
if (semver.gt(latest, currentVersion)) {
|
|
2440
|
+
displayNag(currentVersion, latest);
|
|
2441
|
+
return "fetch-update";
|
|
2442
|
+
}
|
|
2443
|
+
debug("runUpdateCheck: current >= latest, no nag");
|
|
2444
|
+
return "fetch-current";
|
|
2445
|
+
} catch (err) {
|
|
2446
|
+
debug("runUpdateCheck: unexpected error — %s", err instanceof Error ? err.message : String(err));
|
|
2447
|
+
return "error";
|
|
2448
|
+
}
|
|
2449
|
+
}
|
|
2450
|
+
/**
|
|
2451
|
+
* Run the update check at startup. Silent when:
|
|
2452
|
+
* - skip conditions match (CI, NO_UPDATE_NOTIFIER, NODE_ENV=test, non-TTY
|
|
2453
|
+
* stderr, invalid version)
|
|
2454
|
+
* - cache is fresh (< 24h old) — only displays nag if update available.
|
|
2455
|
+
* A cache-hit-current result produces NO output (no "latest version"
|
|
2456
|
+
* message), because no actual check was performed.
|
|
2457
|
+
* - stdin is non-interactive (piped) — runs silently without spinner, but
|
|
2458
|
+
* still shows "You are on the latest version" after a successful fetch
|
|
2459
|
+
*
|
|
2460
|
+
* On a stale/missing cache with interactive stdin, shows a spinner that the
|
|
2461
|
+
* user can dismiss by pressing any key. Ctrl+C restores the terminal and
|
|
2462
|
+
* exits with conventional code 130.
|
|
2463
|
+
*/
|
|
2464
|
+
async function checkForUpdatesUpfront(currentVersion) {
|
|
2465
|
+
debug("checkForUpdatesUpfront: currentVersion=%s", currentVersion);
|
|
2466
|
+
if (shouldSkip(currentVersion)) {
|
|
2467
|
+
debug("checkForUpdatesUpfront: skipped (NO_UPDATE_NOTIFIER / CI / NODE_ENV=test / non-TTY / invalid version)");
|
|
2468
|
+
return;
|
|
2469
|
+
}
|
|
2470
|
+
const cached = await loadCache();
|
|
2471
|
+
if (cached && Date.now() - cached.checkedAt < TTL_MS) {
|
|
2472
|
+
debug("checkForUpdatesUpfront: cache fresh (<%dh), skipping fetch", TTL_MS / 36e5);
|
|
2473
|
+
if (semver.gt(cached.latest, currentVersion)) displayNag(currentVersion, cached.latest);
|
|
2474
|
+
else debug("checkForUpdatesUpfront: current >= latest, no nag");
|
|
2475
|
+
return;
|
|
2476
|
+
}
|
|
2477
|
+
if (cached) debug("checkForUpdatesUpfront: cache stale, refetching");
|
|
2478
|
+
else debug("checkForUpdatesUpfront: no cache, fetching");
|
|
2479
|
+
const stdin = process.stdin;
|
|
2480
|
+
if (stdin.isTTY !== true || typeof stdin.setRawMode !== "function") {
|
|
2481
|
+
debug("checkForUpdatesUpfront: stdin not interactive, running silent check");
|
|
2482
|
+
reportFetchCurrent(await runUpdateCheck(currentVersion));
|
|
2483
|
+
return;
|
|
2484
|
+
}
|
|
2485
|
+
await runCheckWithSpinner(currentVersion);
|
|
2486
|
+
}
|
|
2487
|
+
/**
|
|
2488
|
+
* Surface "You are on the latest version" feedback only when the check
|
|
2489
|
+
* actually performed a fetch and the user is current. Silent on cache hits,
|
|
2490
|
+
* skips, failures, and aborts.
|
|
2491
|
+
*/
|
|
2492
|
+
function reportFetchCurrent(status) {
|
|
2493
|
+
if (status === "fetch-current") log.info(green("You are on the latest version"));
|
|
2494
|
+
}
|
|
2495
|
+
/**
|
|
2496
|
+
* Attach a "press any key to skip" listener to stdin. Returns a cleanup
|
|
2497
|
+
* function that restores raw mode + pauses stdin; safe to call multiple times.
|
|
2498
|
+
* Returns `failed: true` if raw mode could not be enabled (caller should fall
|
|
2499
|
+
* back to a silent check).
|
|
2500
|
+
*
|
|
2501
|
+
* On any keypress: aborts `controller`. On Ctrl+C (byte 0x03): runs cleanup,
|
|
2502
|
+
* stops the spinner with `spinnerMsg`, and exits with conventional code 130.
|
|
2503
|
+
*/
|
|
2504
|
+
function attachStdinSkip(controller, spinner, spinnerMsg) {
|
|
2505
|
+
const stdin = process.stdin;
|
|
2506
|
+
let cleanedUp = false;
|
|
2507
|
+
const cleanup = () => {
|
|
2508
|
+
if (cleanedUp) return;
|
|
2509
|
+
cleanedUp = true;
|
|
2510
|
+
stdin.off("data", onData);
|
|
2511
|
+
try {
|
|
2512
|
+
stdin.setRawMode(false);
|
|
2513
|
+
} catch (err) {
|
|
2514
|
+
debug("attachStdinSkip: setRawMode(false) failed — %s", err instanceof Error ? err.message : String(err));
|
|
2515
|
+
}
|
|
2516
|
+
try {
|
|
2517
|
+
stdin.pause();
|
|
2518
|
+
} catch {}
|
|
2519
|
+
};
|
|
2520
|
+
function onData(buffer) {
|
|
2521
|
+
const byte = buffer[0];
|
|
2522
|
+
debug("attachStdinSkip: stdin byte=0x%s", byte.toString(16).padStart(2, "0"));
|
|
2523
|
+
if (byte === 3) {
|
|
2524
|
+
debug("attachStdinSkip: Ctrl+C (0x03), exiting");
|
|
2525
|
+
cleanup();
|
|
2526
|
+
spinner.stop(spinnerMsg);
|
|
2527
|
+
process.exit(130);
|
|
2528
|
+
}
|
|
2529
|
+
debug("attachStdinSkip: user pressed key, aborting check");
|
|
2530
|
+
controller.abort();
|
|
2531
|
+
}
|
|
2532
|
+
try {
|
|
2533
|
+
stdin.setRawMode(true);
|
|
2534
|
+
stdin.resume();
|
|
2535
|
+
stdin.on("data", onData);
|
|
2536
|
+
return {
|
|
2537
|
+
cleanup,
|
|
2538
|
+
failed: false
|
|
2539
|
+
};
|
|
2540
|
+
} catch (err) {
|
|
2541
|
+
debug("attachStdinSkip: setRawMode failed — %s", err instanceof Error ? err.message : String(err));
|
|
2542
|
+
return {
|
|
2543
|
+
cleanup: () => {},
|
|
2544
|
+
failed: true
|
|
2545
|
+
};
|
|
2546
|
+
}
|
|
2547
|
+
}
|
|
2548
|
+
/**
|
|
2549
|
+
* Show a cancellable spinner while the check runs. Puts stdin in raw mode to
|
|
2550
|
+
* capture individual keypresses via {@link attachStdinSkip}.
|
|
2551
|
+
*/
|
|
2552
|
+
async function runCheckWithSpinner(currentVersion) {
|
|
2553
|
+
debug("runCheckWithSpinner: showing cancellable spinner");
|
|
2554
|
+
const s = spinner();
|
|
2555
|
+
s.start("Checking for updates (press any key to skip)");
|
|
2556
|
+
const controller = new AbortController();
|
|
2557
|
+
const handler = attachStdinSkip(controller, s, "Cancelled");
|
|
2558
|
+
if (handler.failed) {
|
|
2559
|
+
s.stop("");
|
|
2560
|
+
reportFetchCurrent(await runUpdateCheck(currentVersion));
|
|
2561
|
+
return;
|
|
2562
|
+
}
|
|
2563
|
+
let status;
|
|
2564
|
+
try {
|
|
2565
|
+
status = await runUpdateCheck(currentVersion, controller.signal);
|
|
2566
|
+
} finally {
|
|
2567
|
+
handler.cleanup();
|
|
2568
|
+
}
|
|
2569
|
+
if (controller.signal.aborted) {
|
|
2570
|
+
debug("runCheckWithSpinner: spinner dismissed by user");
|
|
2571
|
+
s.stop("Skipped");
|
|
2572
|
+
} 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");
|
|
2575
|
+
else s.stop("");
|
|
2576
|
+
}
|
|
2577
|
+
//#endregion
|
|
2266
2578
|
//#region src/commands/commit-utils.ts
|
|
2267
2579
|
/** Shared recovery menu factory — avoids repeating the same callback set */
|
|
2268
2580
|
function makeRecoveryCallbacks(message) {
|
|
@@ -2697,13 +3009,14 @@ async function restageFormatterModifications(stagedFileList) {
|
|
|
2697
3009
|
}
|
|
2698
3010
|
//#endregion
|
|
2699
3011
|
//#region src/commands/commit.ts
|
|
2700
|
-
async function commitCommand(flags) {
|
|
3012
|
+
async function commitCommand(flags, version) {
|
|
2701
3013
|
debug("commitCommand called", { flags });
|
|
2702
3014
|
await assertGitRepo();
|
|
2703
3015
|
if (flags.retry) return handleRetry();
|
|
2704
3016
|
const repoRoot = await getRepoRoot();
|
|
2705
3017
|
await runPreflightSetupPrompt(repoRoot);
|
|
2706
3018
|
intro("🌿 commit-mint");
|
|
3019
|
+
await checkForUpdatesUpfront(version);
|
|
2707
3020
|
const status = await getStatusShort();
|
|
2708
3021
|
debug("Git status:", status || "(empty)");
|
|
2709
3022
|
if (!status) {
|
|
@@ -3059,93 +3372,147 @@ async function logsCommand(flags) {
|
|
|
3059
3372
|
for (const line of lines) console.log(line);
|
|
3060
3373
|
}
|
|
3061
3374
|
//#endregion
|
|
3062
|
-
//#region src/services/
|
|
3063
|
-
const REGISTRY_URL = "https://registry.npmjs.org/@kyubiware/commit-mint/latest";
|
|
3375
|
+
//#region src/services/updater.ts
|
|
3064
3376
|
const PACKAGE_NAME = "@kyubiware/commit-mint";
|
|
3065
|
-
const TTL_MS = 1440 * 60 * 1e3;
|
|
3066
|
-
const FETCH_TIMEOUT_MS = 5e3;
|
|
3067
|
-
let cachePath = join(os.homedir(), ".cache", "commit-mint", "update-check.json");
|
|
3068
|
-
let fetchImpl = globalThis.fetch;
|
|
3069
3377
|
/**
|
|
3070
|
-
*
|
|
3071
|
-
*
|
|
3378
|
+
* Detect the active package manager from the `npm_config_user_agent` env var.
|
|
3379
|
+
* Format: "<pm>/<version> <...>" — e.g. "npm/10.2.0 node/v20.10.0".
|
|
3380
|
+
* Empty/undefined input and unrecognized prefixes fall back to "npm".
|
|
3072
3381
|
*/
|
|
3073
|
-
function
|
|
3074
|
-
|
|
3075
|
-
|
|
3076
|
-
|
|
3077
|
-
|
|
3078
|
-
|
|
3079
|
-
|
|
3080
|
-
|
|
3081
|
-
|
|
3082
|
-
|
|
3083
|
-
|
|
3084
|
-
|
|
3085
|
-
|
|
3086
|
-
|
|
3087
|
-
const raw = readFileSync(cachePath, "utf8");
|
|
3088
|
-
const parsed = JSON.parse(raw);
|
|
3089
|
-
if (parsed !== null && typeof parsed === "object" && typeof parsed.latest === "string" && typeof parsed.checkedAt === "number") return Promise.resolve(parsed);
|
|
3090
|
-
return Promise.resolve(null);
|
|
3091
|
-
} catch {
|
|
3092
|
-
return Promise.resolve(null);
|
|
3382
|
+
function detectPackageManager(userAgent) {
|
|
3383
|
+
if (!userAgent) {
|
|
3384
|
+
debug("updater: empty userAgent, defaulting to npm");
|
|
3385
|
+
return "npm";
|
|
3386
|
+
}
|
|
3387
|
+
const prefix = userAgent.split("/")[0];
|
|
3388
|
+
switch (prefix) {
|
|
3389
|
+
case "pnpm": return "pnpm";
|
|
3390
|
+
case "yarn": return "yarn";
|
|
3391
|
+
case "bun": return "bun";
|
|
3392
|
+
case "npm": return "npm";
|
|
3393
|
+
default:
|
|
3394
|
+
debug("updater: unknown userAgent prefix '%s', defaulting to npm", prefix);
|
|
3395
|
+
return "npm";
|
|
3093
3396
|
}
|
|
3094
3397
|
}
|
|
3095
|
-
|
|
3096
|
-
|
|
3097
|
-
|
|
3098
|
-
|
|
3099
|
-
|
|
3100
|
-
|
|
3398
|
+
/**
|
|
3399
|
+
* Build the global install shell command for the given package manager.
|
|
3400
|
+
* Returned as a single string suitable for `execa(cmd, [], { shell: true })`.
|
|
3401
|
+
*/
|
|
3402
|
+
function buildUpdateCommand(pm, packageName = PACKAGE_NAME) {
|
|
3403
|
+
switch (pm) {
|
|
3404
|
+
case "npm": return `npm install -g ${packageName}@latest`;
|
|
3405
|
+
case "pnpm": return `pnpm add -g ${packageName}@latest`;
|
|
3406
|
+
case "yarn": return `yarn global add ${packageName}@latest`;
|
|
3407
|
+
case "bun": return `bun add -g ${packageName}@latest`;
|
|
3408
|
+
}
|
|
3101
3409
|
}
|
|
3102
|
-
|
|
3410
|
+
/**
|
|
3411
|
+
* Fetch the latest version from the npm registry via `npm view <pkg> version`.
|
|
3412
|
+
* Returns the trimmed version string on success, or null on any failure
|
|
3413
|
+
* (non-zero exit, empty stdout, thrown error). Never throws.
|
|
3414
|
+
*/
|
|
3415
|
+
async function fetchLatestVersion(packageName = PACKAGE_NAME) {
|
|
3416
|
+
debug("updater: fetching latest version for %s", packageName);
|
|
3103
3417
|
try {
|
|
3104
|
-
const
|
|
3105
|
-
|
|
3106
|
-
|
|
3107
|
-
|
|
3108
|
-
|
|
3109
|
-
|
|
3110
|
-
|
|
3111
|
-
return
|
|
3112
|
-
} finally {
|
|
3113
|
-
clearTimeout(timer);
|
|
3418
|
+
const result = await execa("npm", [
|
|
3419
|
+
"view",
|
|
3420
|
+
packageName,
|
|
3421
|
+
"version"
|
|
3422
|
+
], { reject: false });
|
|
3423
|
+
if (result.exitCode !== 0) {
|
|
3424
|
+
debug("updater: npm view exited %d", result.exitCode);
|
|
3425
|
+
return null;
|
|
3114
3426
|
}
|
|
3115
|
-
|
|
3427
|
+
const trimmed = result.stdout.trim();
|
|
3428
|
+
if (!trimmed) {
|
|
3429
|
+
debug("updater: npm view returned empty stdout");
|
|
3430
|
+
return null;
|
|
3431
|
+
}
|
|
3432
|
+
debug("updater: latest=%s", trimmed);
|
|
3433
|
+
return trimmed;
|
|
3434
|
+
} catch (err) {
|
|
3435
|
+
debug("updater: fetchLatestVersion error — %s", err instanceof Error ? err.message : String(err));
|
|
3116
3436
|
return null;
|
|
3117
3437
|
}
|
|
3118
3438
|
}
|
|
3119
|
-
|
|
3120
|
-
|
|
3121
|
-
|
|
3439
|
+
/**
|
|
3440
|
+
* True iff `latest` is strictly greater than `current` per semver. Returns
|
|
3441
|
+
* false on invalid semver input rather than throwing.
|
|
3442
|
+
*/
|
|
3443
|
+
function isUpdateAvailable(current, latest) {
|
|
3444
|
+
try {
|
|
3445
|
+
return semver.gt(latest, current);
|
|
3446
|
+
} catch (err) {
|
|
3447
|
+
debug("updater: isUpdateAvailable error — %s", err instanceof Error ? err.message : String(err));
|
|
3448
|
+
return false;
|
|
3449
|
+
}
|
|
3122
3450
|
}
|
|
3123
3451
|
/**
|
|
3124
|
-
* Run the
|
|
3125
|
-
*
|
|
3452
|
+
* Run the global install command for the given package manager. Streams
|
|
3453
|
+
* the installer's live output to the user's terminal via `stdio: "inherit"`.
|
|
3454
|
+
* Returns true iff the install exits with code 0.
|
|
3126
3455
|
*/
|
|
3127
|
-
async function
|
|
3128
|
-
|
|
3456
|
+
async function runUpdate(pm, packageName = PACKAGE_NAME) {
|
|
3457
|
+
const command = buildUpdateCommand(pm, packageName);
|
|
3458
|
+
debug("updater: running %s", command);
|
|
3129
3459
|
try {
|
|
3130
|
-
|
|
3131
|
-
|
|
3132
|
-
|
|
3460
|
+
return (await execa(command, [], {
|
|
3461
|
+
shell: true,
|
|
3462
|
+
reject: false,
|
|
3463
|
+
stdio: "inherit"
|
|
3464
|
+
})).exitCode === 0;
|
|
3465
|
+
} catch (err) {
|
|
3466
|
+
debug("updater: runUpdate error — %s", err instanceof Error ? err.message : String(err));
|
|
3467
|
+
return false;
|
|
3468
|
+
}
|
|
3469
|
+
}
|
|
3470
|
+
//#endregion
|
|
3471
|
+
//#region src/commands/update.ts
|
|
3472
|
+
/**
|
|
3473
|
+
* Self-update flow for the installed `cmint` package. Detects the active
|
|
3474
|
+
* package manager from `npm_config_user_agent`, asks the npm registry for the
|
|
3475
|
+
* latest published version, and — if newer — runs the equivalent global
|
|
3476
|
+
* install command after a confirmation prompt (skippable with `--yes`).
|
|
3477
|
+
*
|
|
3478
|
+
* Never throws. Registry failures and install failures are reported through
|
|
3479
|
+
* `p.outro(...)` and `process.exit(1)`; cancellation and "already current"
|
|
3480
|
+
* resolve normally so the CLI exits cleanly.
|
|
3481
|
+
*/
|
|
3482
|
+
async function updateCommand(currentVersion, flags) {
|
|
3483
|
+
p.intro("cmint update");
|
|
3484
|
+
const pm = detectPackageManager(process.env.npm_config_user_agent);
|
|
3485
|
+
p.log.info(`Package manager: ${pm}`);
|
|
3486
|
+
p.log.message("Checking latest version...");
|
|
3487
|
+
const latest = await fetchLatestVersion();
|
|
3488
|
+
if (latest === null) {
|
|
3489
|
+
p.outro(red("Could not reach the npm registry. Check your connection and try again."));
|
|
3490
|
+
process.exit(1);
|
|
3491
|
+
return;
|
|
3492
|
+
}
|
|
3493
|
+
if (!isUpdateAvailable(currentVersion, latest)) {
|
|
3494
|
+
p.outro(`Already up-to-date: v${currentVersion}`);
|
|
3495
|
+
return;
|
|
3496
|
+
}
|
|
3497
|
+
p.log.step(`${dim(currentVersion)} → ${green(latest)}`);
|
|
3498
|
+
const cmd = buildUpdateCommand(pm);
|
|
3499
|
+
if (flags?.yes !== true) {
|
|
3500
|
+
const confirmed = await p.confirm({
|
|
3501
|
+
message: `Run \`${cmd}\`?`,
|
|
3502
|
+
initialValue: true
|
|
3503
|
+
});
|
|
3504
|
+
if (p.isCancel(confirmed) || !confirmed) {
|
|
3505
|
+
p.outro("Update cancelled.");
|
|
3133
3506
|
return;
|
|
3134
3507
|
}
|
|
3135
|
-
|
|
3136
|
-
|
|
3137
|
-
|
|
3138
|
-
|
|
3139
|
-
|
|
3140
|
-
|
|
3141
|
-
|
|
3142
|
-
|
|
3143
|
-
}
|
|
3144
|
-
/** Register the update check to run on `process.beforeExit`. */
|
|
3145
|
-
function checkForUpdates(currentVersion) {
|
|
3146
|
-
process.on("beforeExit", () => {
|
|
3147
|
-
runUpdateCheck(currentVersion);
|
|
3148
|
-
});
|
|
3508
|
+
}
|
|
3509
|
+
p.log.message(`Running ${cyan(cmd)}...`);
|
|
3510
|
+
if (await runUpdate(pm)) {
|
|
3511
|
+
p.outro(green(`Updated to v${latest}`));
|
|
3512
|
+
return;
|
|
3513
|
+
}
|
|
3514
|
+
p.outro(red("Update failed. See output above."));
|
|
3515
|
+
process.exit(1);
|
|
3149
3516
|
}
|
|
3150
3517
|
//#endregion
|
|
3151
3518
|
//#region src/cli.ts
|
|
@@ -3195,27 +3562,39 @@ cli({
|
|
|
3195
3562
|
default: false
|
|
3196
3563
|
}
|
|
3197
3564
|
},
|
|
3198
|
-
commands: [
|
|
3199
|
-
|
|
3200
|
-
|
|
3201
|
-
|
|
3202
|
-
|
|
3203
|
-
|
|
3204
|
-
|
|
3205
|
-
|
|
3206
|
-
|
|
3207
|
-
|
|
3208
|
-
|
|
3209
|
-
|
|
3210
|
-
|
|
3565
|
+
commands: [
|
|
3566
|
+
command({
|
|
3567
|
+
name: "logs",
|
|
3568
|
+
description: "Show debug logs from the last cmint run",
|
|
3569
|
+
flags: { lines: {
|
|
3570
|
+
type: Number,
|
|
3571
|
+
description: "Number of lines to show from the end",
|
|
3572
|
+
alias: "n"
|
|
3573
|
+
} }
|
|
3574
|
+
}, async (argv) => {
|
|
3575
|
+
await logsCommand(argv.flags);
|
|
3576
|
+
}),
|
|
3577
|
+
command({ name: "config" }, async () => {
|
|
3578
|
+
await configCommand();
|
|
3579
|
+
}),
|
|
3580
|
+
command({
|
|
3581
|
+
name: "update",
|
|
3582
|
+
description: "Update cmint to the latest published version",
|
|
3583
|
+
flags: { yes: {
|
|
3584
|
+
type: Boolean,
|
|
3585
|
+
description: "Skip confirmation prompt",
|
|
3586
|
+
alias: "y",
|
|
3587
|
+
default: false
|
|
3588
|
+
} }
|
|
3589
|
+
}, async (argv) => {
|
|
3590
|
+
await updateCommand(version, argv.flags);
|
|
3591
|
+
})
|
|
3592
|
+
]
|
|
3211
3593
|
}, (argv) => {
|
|
3212
3594
|
writeSessionHeader();
|
|
3213
3595
|
setDebug(argv.flags.debug);
|
|
3214
3596
|
if (argv.flags.agent) agentCommand(argv.flags);
|
|
3215
|
-
else
|
|
3216
|
-
checkForUpdates(version);
|
|
3217
|
-
commitCommand(argv.flags);
|
|
3218
|
-
}
|
|
3597
|
+
else commitCommand(argv.flags, version);
|
|
3219
3598
|
});
|
|
3220
3599
|
//#endregion
|
|
3221
3600
|
export {};
|