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 tocache.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
installScopeorupdateCommandwhen you need exact control.
Configuration
Options
| Option | Type | Default | Description |
|---|---|---|---|
currentVersion | string | (required) | The current version of your CLI package. |
packageName | string | (required) | The npm package name to check for updates. |
timeoutMs | number | 5_000 (5s) | Network request timeout. Aborted checks are treated as soft failures. |
registryUrl | string | "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. |
updateCommand | string | ((packageName, packageManager, installScope) => string) | inferred | Override the command shown in the update notice. Recommended when runtime inference is insufficient. |
cache | UpdateNotifierCacheConfig | none | Optional cache configuration for cross-run persistence and dedupe. |
Cache Configuration
When providing cache, it accepts an object with the following properties:
| Property | Type | Default | Description |
|---|---|---|---|
adapter | UpdateNotifierCacheAdapter | (required) | Persistence adapter with read and write methods. |
intervalMs | number | 86_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;