Subcommands
Build nested command trees with automatic routing.
Subcommands let you organize your CLI into a tree of nested commands, like git commit or docker compose up.
Pick a Pattern
Use the pattern that matches how the subcommand is defined:
| Pattern | Use when | Notes |
|---|---|---|
.command(name, cb) | The subcommand is defined inline | Best default for small or nested command trees |
.sub(name) + .command(builder) | The subcommand lives in another file or should inherit parent flags | Best default for modular CLIs |
new Crust(name) + .command(builder) | The subcommand is intentionally standalone | Does not inherit parent flags |
Inline Subcommands
Use .command(name, cb) when the subcommand is defined in place:
import { Crust } from "@crustjs/core";
import { helpPlugin } from "@crustjs/plugins";
const main = new Crust("my-cli")
.meta({ description: "My CLI tool" })
.use(helpPlugin())
.command("build", (cmd) =>
cmd
.meta({ description: "Build the project" })
.flags({ minify: { type: "boolean", default: true } })
.run(({ flags }) => {
console.log(`Building... (minify: ${flags.minify})`);
})
)
.command("dev", (cmd) =>
cmd
.meta({ description: "Start dev server" })
.flags({ port: { type: "number", default: 3000, short: "p" } })
.run(({ flags }) => {
console.log(`Dev server on port ${flags.port}`);
})
);
await main.execute();my-cli build # Runs the build command
my-cli dev --port 8080 # Runs the dev command
my-cli --help # Shows help with available subcommandsHow Routing Works
When Crust receives argv, it walks the subcommand tree before parsing flags or arguments. Each token is checked against subcommand names — if it matches, Crust recurses into that subcommand. If no match is found, the token is treated as a positional arg (when the command has .run()) or throws a COMMAND_NOT_FOUND error (when it doesn't).
This means subcommand names are consumed before flag parsing, so my-cli build --minify first resolves build, then parses --minify against the build command's flags. See the resolveCommand() reference for the detailed step-by-step algorithm.
Nested Subcommands
Subcommands can be nested arbitrarily deep:
const main = new Crust("my-cli")
.command("create", (create) =>
create
.meta({ description: "Create resources" })
.command("component", (cmd) =>
cmd
.meta({ description: "Create a component" })
.args([{ name: "name", type: "string", required: true }])
.run(({ args }) => {
console.log(`Creating component: ${args.name}`);
})
)
.command("plugin", (cmd) =>
cmd
.meta({ description: "Create a plugin" })
.args([{ name: "name", type: "string", required: true }])
.run(({ args }) => {
console.log(`Creating plugin: ${args.name}`);
})
)
);my-cli create component Button
my-cli create plugin authContainer Commands
A command with subcommands but no .run() handler is a container command. It exists purely to group subcommands:
const main = new Crust("my-cli")
.meta({ description: "My CLI tool" })
.command("build", (cmd) => cmd.run(() => { /* ... */ }))
.command("dev", (cmd) => cmd.run(() => { /* ... */ }));
// No .run() on main — this is a containerWhen a user runs a container command directly (e.g., my-cli with no subcommand), the help plugin automatically shows help with the available subcommands.
Commands with Both .run() and Subcommands
A command can have both .run() and subcommands. In this case:
- If the first argument matches a subcommand name, it routes to that subcommand
- Otherwise, the arguments are treated as positionals for the parent command
const main = new Crust("my-cli")
.args([{ name: "file", type: "string" }])
.command("build", (cmd) => cmd.run(() => { /* ... */ }))
.command("dev", (cmd) => cmd.run(() => { /* ... */ }))
.run(({ args }) => {
// Runs when no subcommand matches
console.log(`Processing file: ${args.file}`);
});my-cli build # Routes to build subcommand
my-cli readme.md # Runs parent command with file="readme.md"File-Splitting Pattern
Use .sub() when the subcommand lives in another file or should inherit parent inherit: true flags:
import { Crust } from "@crustjs/core";
export const app = new Crust("my-cli")
.flags({ verbose: { type: "boolean", short: "v", inherit: true } });import { app } from "../shared.ts";
export const deployCmd = app.sub("deploy")
.meta({ description: "Deploy to production" })
.flags({ env: { type: "string", required: true } })
.run(({ flags }) => {
flags.verbose; // boolean | undefined — inherited and typed!
flags.env; // string
});import { app } from "./shared.ts";
import { deployCmd } from "./commands/deploy.ts";
import { helpPlugin } from "@crustjs/plugins";
await app
.use(helpPlugin())
.command(deployCmd)
.execute();.sub() creates a child builder pre-typed with the parent's inheritable flags. See the .sub() reference for details.
Standalone Builders
.command(builder) also accepts a standalone builder created with new Crust(name):
const app = new Crust("my-cli").flags({
verbose: { type: "boolean", inherit: true },
});
const standalone = new Crust("doctor").run(() => {
console.log("doctor");
});
await app.command(standalone).execute();This works, but standalone is isolated from app, so it does not inherit app's inherit: true flags. If you want inherited flags or parent-aware typing, use .sub(name) instead:
const deploy = app.sub("deploy").run(({ flags }) => {
flags.verbose; // boolean | undefined
});
await app.command(deploy).execute();Unknown Subcommands
When a user types an unknown subcommand on a container command (no .run()), Crust throws a COMMAND_NOT_FOUND error with structured details:
{
input: "buld", // What the user typed
available: ["build", "dev"], // Valid subcommands
commandPath: ["my-cli"], // Path to the parent
parentCommand: main, // The parent command node
}The Autocomplete Plugin catches these errors and provides "Did you mean?" suggestions.
Command Path
Crust tracks the full command path during routing. This is used by the help plugin to generate correct usage text:
$ my-cli create component --help
my-cli create component - Create a component
USAGE:
my-cli create component <name> [options]The path ["my-cli", "create", "component"] is available in the CommandRoute returned by resolveCommand.