@kyubiware/commit-mint 0.7.2 → 0.7.4

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 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.2",
32
+ version: "0.7.4",
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" },
@@ -2311,7 +2311,6 @@ async function agentCommand(flags) {
2311
2311
  //#endregion
2312
2312
  //#region src/services/update-check.ts
2313
2313
  const REGISTRY_URL = "https://registry.npmjs.org/-/package/@kyubiware/commit-mint/dist-tags";
2314
- const PACKAGE_NAME = "@kyubiware/commit-mint";
2315
2314
  const TTL_MS = 1440 * 60 * 1e3;
2316
2315
  const FETCH_TIMEOUT_MS = 5e3;
2317
2316
  let cachePath = join(os.homedir(), ".cache", "commit-mint", "update-check.json");
@@ -2396,7 +2395,7 @@ async function fetchLatest(parentSignal) {
2396
2395
  }
2397
2396
  function displayNag(current, latest) {
2398
2397
  debug("displayNag: %s → %s", current, latest);
2399
- const message = `Update available: ${yellow(current)} → ${green(latest)}\nRun ${cyan(`npm update -g ${PACKAGE_NAME}`)} to update`;
2398
+ const message = `Update available: ${yellow(current)} → ${green(latest)}\nRun ${cyan("cmint update")} to update`;
2400
2399
  log.warn(message);
2401
2400
  }
2402
2401
  /**
@@ -2404,11 +2403,16 @@ function displayNag(current, latest) {
2404
2403
  * {@link checkForUpdatesUpfront}. Accepts an optional AbortSignal that
2405
2404
  * propagates to the underlying fetch — used by the cancellable spinner.
2406
2405
  *
2406
+ * When an update is available, {@link onNag} is invoked (defaulting to
2407
+ * {@link displayNag}). The cancellable spinner path passes a capturing
2408
+ * callback so it can stop the spinner BEFORE the nag prints — otherwise
2409
+ * `log.warn` interleaves with the spinner line and leaves it on screen.
2410
+ *
2407
2411
  * Returns an {@link UpdateCheckStatus} so the caller can distinguish a
2408
2412
  * real fetch that found the user current (eligible for "You are on the
2409
2413
  * latest version" feedback) from a silent cache hit.
2410
2414
  */
2411
- async function runUpdateCheck(currentVersion, parentSignal) {
2415
+ async function runUpdateCheck(currentVersion, parentSignal, onNag = displayNag) {
2412
2416
  debug("runUpdateCheck: currentVersion=%s", currentVersion);
2413
2417
  if (shouldSkip(currentVersion)) {
2414
2418
  debug("runUpdateCheck: skipped (NO_UPDATE_NOTIFIER / CI / NODE_ENV=test / non-TTY / invalid version)");
@@ -2419,7 +2423,7 @@ async function runUpdateCheck(currentVersion, parentSignal) {
2419
2423
  if (cached && Date.now() - cached.checkedAt < TTL_MS) {
2420
2424
  debug("runUpdateCheck: cache fresh (<%dh), skipping fetch", TTL_MS / 36e5);
2421
2425
  if (semver.gt(cached.latest, currentVersion)) {
2422
- displayNag(currentVersion, cached.latest);
2426
+ onNag(currentVersion, cached.latest);
2423
2427
  return "cache-update";
2424
2428
  }
2425
2429
  debug("runUpdateCheck: current >= latest, no nag");
@@ -2437,7 +2441,7 @@ async function runUpdateCheck(currentVersion, parentSignal) {
2437
2441
  checkedAt: Date.now()
2438
2442
  });
2439
2443
  if (semver.gt(latest, currentVersion)) {
2440
- displayNag(currentVersion, latest);
2444
+ onNag(currentVersion, latest);
2441
2445
  return "fetch-update";
2442
2446
  }
2443
2447
  debug("runUpdateCheck: current >= latest, no nag");
@@ -2560,9 +2564,15 @@ async function runCheckWithSpinner(currentVersion) {
2560
2564
  reportFetchCurrent(await runUpdateCheck(currentVersion));
2561
2565
  return;
2562
2566
  }
2567
+ const captured = { nag: null };
2563
2568
  let status;
2564
2569
  try {
2565
- status = await runUpdateCheck(currentVersion, controller.signal);
2570
+ status = await runUpdateCheck(currentVersion, controller.signal, (current, latest) => {
2571
+ captured.nag = {
2572
+ current,
2573
+ latest
2574
+ };
2575
+ });
2566
2576
  } finally {
2567
2577
  handler.cleanup();
2568
2578
  }
@@ -2570,8 +2580,10 @@ async function runCheckWithSpinner(currentVersion) {
2570
2580
  debug("runCheckWithSpinner: spinner dismissed by user");
2571
2581
  s.stop("Skipped");
2572
2582
  } 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");
2583
+ else if (status === "fetch-update" || status === "cache-update") {
2584
+ s.stop("");
2585
+ if (captured.nag) displayNag(captured.nag.current, captured.nag.latest);
2586
+ } else if (status === "fetch-failed-or-aborted") s.stop("Update check failed");
2575
2587
  else s.stop("");
2576
2588
  }
2577
2589
  //#endregion
@@ -3372,6 +3384,149 @@ async function logsCommand(flags) {
3372
3384
  for (const line of lines) console.log(line);
3373
3385
  }
3374
3386
  //#endregion
3387
+ //#region src/services/updater.ts
3388
+ const PACKAGE_NAME = "@kyubiware/commit-mint";
3389
+ /**
3390
+ * Detect the active package manager from the `npm_config_user_agent` env var.
3391
+ * Format: "<pm>/<version> <...>" — e.g. "npm/10.2.0 node/v20.10.0".
3392
+ * Empty/undefined input and unrecognized prefixes fall back to "npm".
3393
+ */
3394
+ function detectPackageManager(userAgent) {
3395
+ if (!userAgent) {
3396
+ debug("updater: empty userAgent, defaulting to npm");
3397
+ return "npm";
3398
+ }
3399
+ const prefix = userAgent.split("/")[0];
3400
+ switch (prefix) {
3401
+ case "pnpm": return "pnpm";
3402
+ case "yarn": return "yarn";
3403
+ case "bun": return "bun";
3404
+ case "npm": return "npm";
3405
+ default:
3406
+ debug("updater: unknown userAgent prefix '%s', defaulting to npm", prefix);
3407
+ return "npm";
3408
+ }
3409
+ }
3410
+ /**
3411
+ * Build the global install shell command for the given package manager.
3412
+ * Returned as a single string suitable for `execa(cmd, [], { shell: true })`.
3413
+ */
3414
+ function buildUpdateCommand(pm, packageName = PACKAGE_NAME) {
3415
+ switch (pm) {
3416
+ case "npm": return `npm install -g ${packageName}@latest`;
3417
+ case "pnpm": return `pnpm add -g ${packageName}@latest`;
3418
+ case "yarn": return `yarn global add ${packageName}@latest`;
3419
+ case "bun": return `bun add -g ${packageName}@latest`;
3420
+ }
3421
+ }
3422
+ /**
3423
+ * Fetch the latest version from the npm registry via `npm view <pkg> version`.
3424
+ * Returns the trimmed version string on success, or null on any failure
3425
+ * (non-zero exit, empty stdout, thrown error). Never throws.
3426
+ */
3427
+ async function fetchLatestVersion(packageName = PACKAGE_NAME) {
3428
+ debug("updater: fetching latest version for %s", packageName);
3429
+ try {
3430
+ const result = await execa("npm", [
3431
+ "view",
3432
+ packageName,
3433
+ "version"
3434
+ ], { reject: false });
3435
+ if (result.exitCode !== 0) {
3436
+ debug("updater: npm view exited %d", result.exitCode);
3437
+ return null;
3438
+ }
3439
+ const trimmed = result.stdout.trim();
3440
+ if (!trimmed) {
3441
+ debug("updater: npm view returned empty stdout");
3442
+ return null;
3443
+ }
3444
+ debug("updater: latest=%s", trimmed);
3445
+ return trimmed;
3446
+ } catch (err) {
3447
+ debug("updater: fetchLatestVersion error — %s", err instanceof Error ? err.message : String(err));
3448
+ return null;
3449
+ }
3450
+ }
3451
+ /**
3452
+ * True iff `latest` is strictly greater than `current` per semver. Returns
3453
+ * false on invalid semver input rather than throwing.
3454
+ */
3455
+ function isUpdateAvailable(current, latest) {
3456
+ try {
3457
+ return semver.gt(latest, current);
3458
+ } catch (err) {
3459
+ debug("updater: isUpdateAvailable error — %s", err instanceof Error ? err.message : String(err));
3460
+ return false;
3461
+ }
3462
+ }
3463
+ /**
3464
+ * Run the global install command for the given package manager. Streams
3465
+ * the installer's live output to the user's terminal via `stdio: "inherit"`.
3466
+ * Returns true iff the install exits with code 0.
3467
+ */
3468
+ async function runUpdate(pm, packageName = PACKAGE_NAME) {
3469
+ const command = buildUpdateCommand(pm, packageName);
3470
+ debug("updater: running %s", command);
3471
+ try {
3472
+ return (await execa(command, [], {
3473
+ shell: true,
3474
+ reject: false,
3475
+ stdio: "inherit"
3476
+ })).exitCode === 0;
3477
+ } catch (err) {
3478
+ debug("updater: runUpdate error — %s", err instanceof Error ? err.message : String(err));
3479
+ return false;
3480
+ }
3481
+ }
3482
+ //#endregion
3483
+ //#region src/commands/update.ts
3484
+ /**
3485
+ * Self-update flow for the installed `cmint` package. Detects the active
3486
+ * package manager from `npm_config_user_agent`, asks the npm registry for the
3487
+ * latest published version, and — if newer — runs the equivalent global
3488
+ * install command after a confirmation prompt (skippable with `--yes`).
3489
+ *
3490
+ * Never throws. Registry failures and install failures are reported through
3491
+ * `p.outro(...)` and `process.exit(1)`; cancellation and "already current"
3492
+ * resolve normally so the CLI exits cleanly.
3493
+ */
3494
+ async function updateCommand(currentVersion, flags) {
3495
+ p.intro("cmint update");
3496
+ const pm = detectPackageManager(process.env.npm_config_user_agent);
3497
+ p.log.info(`Package manager: ${pm}`);
3498
+ p.log.message("Checking latest version...");
3499
+ const latest = await fetchLatestVersion();
3500
+ if (latest === null) {
3501
+ p.outro(red("Could not reach the npm registry. Check your connection and try again."));
3502
+ process.exit(1);
3503
+ return;
3504
+ }
3505
+ if (!isUpdateAvailable(currentVersion, latest)) {
3506
+ p.outro(`Already up-to-date: v${currentVersion}`);
3507
+ return;
3508
+ }
3509
+ p.log.step(`${dim(currentVersion)} → ${green(latest)}`);
3510
+ const cmd = buildUpdateCommand(pm);
3511
+ if (flags?.yes !== true) {
3512
+ const confirmed = await p.confirm({
3513
+ message: `Run \`${cmd}\`?`,
3514
+ initialValue: true
3515
+ });
3516
+ if (p.isCancel(confirmed) || !confirmed) {
3517
+ p.outro("Update cancelled.");
3518
+ return;
3519
+ }
3520
+ }
3521
+ p.log.message(`Running ${cyan(cmd)}...`);
3522
+ if (await runUpdate(pm)) {
3523
+ p.outro(green(`Updated to v${latest}`));
3524
+ return;
3525
+ }
3526
+ p.outro(red("Update failed. See output above."));
3527
+ process.exit(1);
3528
+ }
3529
+ //#endregion
3375
3530
  //#region src/cli.ts
3376
3531
  const { version } = package_default;
3377
3532
  cli({
@@ -3419,19 +3574,34 @@ cli({
3419
3574
  default: false
3420
3575
  }
3421
3576
  },
3422
- commands: [command({
3423
- name: "logs",
3424
- description: "Show debug logs from the last cmint run",
3425
- flags: { lines: {
3426
- type: Number,
3427
- description: "Number of lines to show from the end",
3428
- alias: "n"
3429
- } }
3430
- }, async (argv) => {
3431
- await logsCommand(argv.flags);
3432
- }), command({ name: "config" }, async () => {
3433
- await configCommand();
3434
- })]
3577
+ commands: [
3578
+ command({
3579
+ name: "logs",
3580
+ description: "Show debug logs from the last cmint run",
3581
+ flags: { lines: {
3582
+ type: Number,
3583
+ description: "Number of lines to show from the end",
3584
+ alias: "n"
3585
+ } }
3586
+ }, async (argv) => {
3587
+ await logsCommand(argv.flags);
3588
+ }),
3589
+ command({ name: "config" }, async () => {
3590
+ await configCommand();
3591
+ }),
3592
+ command({
3593
+ name: "update",
3594
+ description: "Update cmint to the latest published version",
3595
+ flags: { yes: {
3596
+ type: Boolean,
3597
+ description: "Skip confirmation prompt",
3598
+ alias: "y",
3599
+ default: false
3600
+ } }
3601
+ }, async (argv) => {
3602
+ await updateCommand(version, argv.flags);
3603
+ })
3604
+ ]
3435
3605
  }, (argv) => {
3436
3606
  writeSessionHeader();
3437
3607
  setDebug(argv.flags.debug);