Lifecycle & Hooks
Understand the command execution lifecycle and preRun, run, postRun hooks.
Every command supports three lifecycle hooks: .preRun(), .run(), and .postRun(). These let you add initialization and cleanup logic around your main command handler.
Lifecycle Hooks
import { Crust } from "@crustjs/core";
const cmd = new Crust("deploy")
.preRun(({ args, flags }) => {
console.log("Validating configuration...");
})
.run(({ args, flags }) => {
console.log("Deploying...");
})
.postRun(({ args, flags }) => {
console.log("Cleaning up...");
});Execution Order
preRun -> run -> postRun.preRun()— Called beforerun(). Use for initialization, validation, or setup.run()— The main command handler.postRun()— Called afterrun(), even ifrun()throws. Use for cleanup or teardown
postRun runs in a finally block, so it executes even when run() throws an error. The error is still re-thrown after postRun completes.
Async Support
All hooks support async functions:
new Crust("migrate")
.preRun(async () => {
await checkDatabaseConnection();
})
.run(async () => {
await runMigrations();
})
.postRun(async () => {
await closeDatabaseConnection();
})Hook Context
Each hook receives the same CrustCommandContext object:
interface CrustCommandContext<A, F> {
args: InferArgs<A>; // Parsed positional arguments
flags: InferFlags<F>; // Parsed flags (inherited + local merged)
rawArgs: string[]; // Arguments after `--`
command: CommandNode; // The resolved command node
}All hooks see the same parsed input — there's no data transformation between hooks.
Full Execution Lifecycle
When you call .execute(), Crust runs through four phases:
1. Initialization
Plugins are collected from the command tree (root-first, depth-first) and each setup() hook runs sequentially. If any setup() throws, the lifecycle aborts immediately. After setup, the command tree is frozen and plugin warnings are surfaced.
2. Routing & Parsing
resolveCommand() walks argv against the subcommand tree, then parseArgs() parses and coerces args and flags for the matched command. Parsing errors (unknown flags, bad types) still pass through the middleware chain. Required-value validation is deferred to after middleware — this allows plugins like helpPlugin to intercept --help before missing-arg errors are surfaced.
3. Middleware & Hooks
The middleware chain uses an onion model — each plugin wraps the next, with command hooks at the center:
middleware A → middleware B → preRun → run → postRunpreRun()throws →run()is skipped,postRun()still runspostRun()always runs, even on error. If bothrunandpostRunthrow, the original error is preserved
4. Error Handling
.execute() catches all errors — they never propagate to the caller. Errors are printed to stderr and process.exitCode is set to 1.
Practical Example
const deploy = new Crust("deploy")
.meta({ description: "Deploy to production" })
.flags({
env: { type: "string", required: true, description: "Target environment" },
dryRun: { type: "boolean", description: "Simulate without deploying", short: "d" },
})
.preRun(async ({ flags }) => {
// Validate before running
if (flags.env === "production" && flags.dryRun) {
console.log("Dry run for production deployment");
}
})
.run(async ({ flags }) => {
if (flags.dryRun) {
console.log(`[DRY RUN] Would deploy to ${flags.env}`);
return;
}
console.log(`Deploying to ${flags.env}...`);
// deployment logic
})
.postRun(async ({ flags }) => {
// Always runs — send notification regardless of success/failure
console.log(`Deployment to ${flags.env} finished`);
});