Plugins
Extend your CLI with the middleware-based plugin system.
Crust's plugin system lets you extend CLI behavior through two phases: setup (one-time initialization) and middleware (per-execution interception).
Using Plugins
Register plugins with .use():
import { Crust } from "@crustjs/core";
import {
helpPlugin,
versionPlugin,
autoCompletePlugin,
} from "@crustjs/plugins";
const main = new Crust("my-cli")
.meta({ description: "My CLI" })
.use(versionPlugin("1.0.0"))
.use(autoCompletePlugin({ mode: "help" }))
.use(helpPlugin())
.run(() => {
console.log("Running!");
});
await main.execute();Crust ships with these official plugins:
| Plugin | Description |
|---|---|
helpPlugin() | Adds --help / -h and auto-generates help text |
versionPlugin() | Adds --version / -v to the root command |
autoCompletePlugin() | Suggests corrections for mistyped subcommands |
updateNotifierPlugin() | Checks npm for newer versions and displays an update notice |
Plugin Order
Plugins execute in the order they're registered. Each middleware wraps the next in a chain, so the first plugin registered is the outermost wrapper:
new Crust("my-cli")
.use(versionPlugin("1.0.0")) // Runs first, can intercept early
.use(autoCompletePlugin()) // Runs second, catches routing errors
.use(helpPlugin()) // Runs third, handles --helpA typical order is: version -> autocomplete -> help.
Plugin Lifecycle
Setup Phase
The setup function runs once during CLI initialization, before any command parsing. It's used to register flags or perform one-time configuration:
const myPlugin: CrustPlugin = {
name: "my-plugin",
setup(context, actions) {
// context.rootCommand — the root command node
// context.argv — raw argv
// context.state — plugin state store
// Inject a flag into the root command
actions.addFlag(context.rootCommand, "debug", {
type: "boolean",
description: "Enable debug mode",
});
},
};Available setup actions:
| Action | Description |
|---|---|
addFlag(command, name, def) | Inject a flag into a command's effective flags (overrides if name exists) |
addSubCommand(parent, name, command) | Inject a subcommand (skipped if user already defined one with the same name) |
Injecting Subcommands
Plugins can register subcommands during setup. If the user already defined a subcommand with the same name, the plugin's injection is skipped — user definitions always take priority.
Both addFlag overrides and addSubCommand skips are silent at runtime but produce warnings during crust build pre-compile validation, so you can catch unintentional conflicts before shipping.
import { createCommandNode } from "@crustjs/core";
const myPlugin: CrustPlugin = {
name: "my-plugin",
setup(context, actions) {
const statusNode = createCommandNode("status");
statusNode.meta.description = "Show status";
statusNode.run = () => {
console.log("Status: OK");
};
// Inject "status" as a subcommand of the root command
actions.addSubCommand(context.rootCommand, "status", statusNode);
},
};Middleware Phase
The middleware function runs for each CLI invocation. It receives a context and a next function to pass control to the next middleware (or the command itself):
const myPlugin: CrustPlugin = {
name: "my-plugin",
middleware(context, next) {
// context.route — resolved command route (or null)
// context.input — parsed args/flags (or null)
// context.rootCommand — the root command node
// context.argv — raw argv
// context.state — plugin state store
if (context.input?.flags.debug) {
console.log("[debug] Route:", context.route?.commandPath);
}
// Pass control to the next middleware or command execution
return next();
},
};Always call next() unless you intentionally want to prevent the command from
running (e.g., when the help plugin intercepts --help).
Writing a Custom Plugin
A CrustPlugin is a plain object with optional name, setup, and middleware:
import type { CrustPlugin } from "@crustjs/core";
function timingPlugin(): CrustPlugin {
return {
name: "timing",
async middleware(context, next) {
const start = performance.now();
await next();
const duration = (performance.now() - start).toFixed(2);
console.log(`Completed in ${duration}ms`);
},
};
}Use it like any other plugin:
new Crust("my-cli")
.use(timingPlugin())
.use(helpPlugin())Plugin State
Plugins can share state via context.state, a key-value store available in both setup and middleware phases:
function authPlugin(): CrustPlugin {
return {
name: "auth",
setup(context) {
context.state.set("auth.token", process.env.API_TOKEN);
},
async middleware(context, next) {
const token = context.state.get<string>("auth.token");
if (!token) {
console.error("No API token found");
process.exitCode = 1;
return;
}
await next();
},
};
}The state API:
| Method | Description |
|---|---|
state.get<T>(key) | Get a value (returns T | undefined) |
state.set(key, value) | Set a value |
state.has(key) | Check if a key exists |
state.delete(key) | Delete a key |
Middleware Context
The full middleware context:
interface MiddlewareContext {
readonly argv: readonly string[]; // Raw argv
readonly rootCommand: CommandNode; // Root command node
readonly state: PluginState; // Plugin state store
route: CommandRoute | null; // Resolved route (null if routing failed)
input: ParseResult | null; // Parsed input (null if parsing failed)
}route and input may be null if routing or parsing failed — the error is re-thrown inside the middleware chain, allowing error-handling plugins (like autocomplete) to catch it.
Plugin Collection
Plugins are collected from the entire command tree during .execute(). Root plugins come first, then depth-first through subcommands. This means you can register plugins on specific subcommands if needed, though most plugins are registered on the root command.