Crust logoCrust

Update Notifier

Check npm for newer versions and display an update notice.

The update notifier plugin checks the npm registry for newer versions of your package and displays a notice after command execution when an update is available.

Usage

import { Crust } from "@crustjs/core";
import { updateNotifierPlugin } from "@crustjs/plugins";
import pkg from "../package.json";

const main = new Crust("my-cli")
  .meta({ description: "My CLI tool" })
  .use(updateNotifierPlugin({ packageName: pkg.name, currentVersion: pkg.version }))
  .run(() => {
    console.log("Hello!");
  });

await main.execute();

You are responsible for passing packageName and currentVersion — typically sourced from your package.json.

Behavior

  • No persistence by default — Out of the box, the plugin does not persist notifier state across runs.
  • Optional cache adapter — If you provide cache, checks are reused up to cache.intervalMs (default 24h) and notifications are deduped across runs.
  • Non-blocking — The update check runs after your command handler completes. It never delays command execution.
  • Soft failure — All internal errors (network timeouts, registry failures, cache errors, malformed responses) are silently swallowed. The plugin never affects exit codes or command output.
  • Stderr output — The update notice is written to stderr so it does not interfere with piped stdout.
  • Package-manager-aware command — The upgrade hint is inferred from the runtime environment by default and can be overridden.
  • Scope-aware command — The notifier also infers local vs global installs with best-effort heuristics. Use installScope or updateCommand when you need exact control.

Configuration

Options

OptionTypeDefaultDescription
currentVersionstring(required)The current version of your CLI package.
packageNamestring(required)The npm package name to check for updates.
timeoutMsnumber5_000 (5s)Network request timeout. Aborted checks are treated as soft failures.
registryUrlstring"https://registry.npmjs.org"Custom npm registry URL.
packageManager"auto" | "npm" | "pnpm" | "yarn" | "bun""auto"Package manager used when building the default update command.
installScope"auto" | "local" | "global""auto"Install scope used when building the default update command.
updateCommandstring | ((packageName, packageManager, installScope) => string)inferredOverride the command shown in the update notice. Recommended when runtime inference is insufficient.
cacheUpdateNotifierCacheConfignoneOptional cache configuration for cross-run persistence and dedupe.

Cache Configuration

When providing cache, it accepts an object with the following properties:

PropertyTypeDefaultDescription
adapterUpdateNotifierCacheAdapter(required)Persistence adapter with read and write methods.
intervalMsnumber86_400_000 (24h)Minimum interval in milliseconds between network checks.

Optional Persistence with @crustjs/store

If you want cross-run cache behavior, pass an adapter. @crustjs/store can be used directly via a thin wrapper:

import { stateDir, createStore } from "@crustjs/store";
import { updateNotifierPlugin } from "@crustjs/plugins";
import type { UpdateNotifierCacheAdapter } from "@crustjs/plugins";

const store = createStore({
  dirPath: stateDir("my-cli"), // Replace with your package name
  name: "update-notifier",
  fields: {
    lastCheckedAt: { type: "number", default: 0 },
    latestVersion: { type: "string" },
    lastNotifiedVersion: { type: "string" },
  },
});

const cacheAdapter: UpdateNotifierCacheAdapter = {
  read: async () => {
    const state = await store.read();
    return {
      lastCheckedAt: state.lastCheckedAt,
      latestVersion: state.latestVersion,
      lastNotifiedVersion: state.lastNotifiedVersion,
    };
  },
  write: async (state) => {
    await store.write({
      lastCheckedAt: state.lastCheckedAt,
      latestVersion: state.latestVersion,
      lastNotifiedVersion: state.lastNotifiedVersion,
    });
  },
};

updateNotifierPlugin({
  packageName: "my-cli",
  currentVersion: "1.0.0",
  cache: { adapter: cacheAdapter },
});

For a globally installed Bun CLI, you can set the scope directly:

updateNotifierPlugin({
  packageName: "my-cli",
  currentVersion: "1.0.0",
  packageManager: "bun",
  installScope: "global",
});

Or provide an explicit command:

updateNotifierPlugin({
  packageName: "my-cli",
  currentVersion: "1.0.0",
  updateCommand: "bun add -g my-cli@latest",
});

write receives a single argument (state). If you pass a function with two parameters, TypeScript will report a signature mismatch.

Version comparison uses standard semver (major.minor.patch). Prerelease suffixes are stripped before comparison — 1.2.3-beta.1 is treated as 1.2.3.

Types

interface UpdateNotifierPluginOptions {
  currentVersion: string;
  packageName: string;
  timeoutMs?: number;
  registryUrl?: string;
  packageManager?: UpdateNotifierPackageManager | "auto";
  installScope?: UpdateNotifierInstallScope | "auto";
  updateCommand?:
    | string
    | ((
        packageName: string,
        packageManager: UpdateNotifierPackageManager,
        installScope: UpdateNotifierInstallScope,
      ) => string);
  cache?: UpdateNotifierCacheConfig;
}

interface UpdateNotifierCacheConfig {
  adapter: UpdateNotifierCacheAdapter;
  intervalMs?: number;
}

interface UpdateNotifierCacheAdapter {
  read(): Promise<
    | {
        lastCheckedAt: number;
        latestVersion?: string;
        lastNotifiedVersion?: string;
      }
    | null
    | undefined
  >;
  write(
    state: {
      lastCheckedAt: number;
      latestVersion?: string;
      lastNotifiedVersion?: string;
    },
  ): Promise<void>;
}

type UpdateNotifierPackageManager = "npm" | "pnpm" | "yarn" | "bun";
type UpdateNotifierInstallScope = "local" | "global";

function updateNotifierPlugin(
  options: UpdateNotifierPluginOptions,
): CrustPlugin;

On this page