Crust logoCrust

Commands

Define commands in one file or split them into modules.

Create a CLI with new Crust(name), define what it does with .action(), and call .execute() to run it.

src/cli.ts
import { Crust } from "@crustjs/core";

const app = new Crust("hello").action(({ stdout }) => {
	stdout("Hello!");
});

await app.execute();
$ bun src/cli.ts
Hello!

Choose how to add subcommands based on your app's size and how you want to organize and reuse its commands:

  • Define inline with .command() for small CLIs or short subcommands used only by this app.
  • Use defineCommand() with .add() when a growing CLI needs separate command files or reusable definitions.

You can use both approaches in the same app.

Grow in one file

Keep small subcommands alongside the root with .command(). Its callback defines the subcommand's flags and action, and can call .command() again to nest another level.

src/cli.ts
import { Crust } from "@crustjs/core";

const tool = new Crust("tool").command("build", (command) =>
	command.flags({ name: "minify", type: "boolean" }).action(({ flags, stdout }) => {
		stdout(`minify: ${flags. ?? false}`);
	}),
);

await tool.execute();
$ tool build --minify
minify: true

Split across files

src/
├── commands/greet.ts
├── app.ts
└── cli.ts

Move a subcommand into its own file with defineCommand(), then attach it with .add(). Keep shared app setup in app.ts and the single .execute() call in cli.ts.

Define a command

src/commands/greet.ts
import { defineCommand } from "@crustjs/core";

export const greetCommand = defineCommand("greet", { description: "Greet someone" }, (command) =>
	command
		.args({ name: "name", type: "string", default: "world" })
		.flags({ name: "greeting", type: "string", default: "Hello", short: "g" })
		.action(({ args, flags, stdout }) => {
			stdout(`${flags.greeting}, ${args.name}!`);
		}),
);

Importing a command definition does not run its action. The optional second argument supplies metadata, such as the description shown in help. See the defineCommand() options for aliases, custom usage, additional sections, and hiding commands from listings.

Define the root

src/app.ts
import { Crust } from "@crustjs/core";
import { help } from "@crustjs/extensions";

export const app = new Crust("my-cli", {
	description: "Print greetings",
}).extend(help());

Set the CLI's name and description here, along with shared Extensions such as help().

Compose and execute

src/cli.ts
import { app } from "./app.ts";
import { greetCommand } from "./commands/greet.ts";

await app.add(greetCommand).execute();

Add your commands before calling .execute().

$ my-cli greet Ada
Hello, Ada!

Run commands from code

.execute() is the terminal entry point: it parses argv, renders errors, and sets the exit code. To call a command from your own code, such as a script, server, or test, use .run() with a typed path and structured input instead.

src/script.ts
import { app } from "./app.ts";
import { greetCommand } from "./commands/greet.ts";

const cli = app.add(greetCommand);

const outcome = await cli.run(["greet"], { args: { name: "Ada" }, flags: { greeting: "Hi" } });
if (outcome.status === "failed") throw outcome.error;
console.log(outcome.stdout); // => Hi, Ada!

run() resolves to an outcome with the captured stdout and stderr and never renders errors, changes process.exitCode, or applies flag env fallbacks. Check status before reading the action's result. See the run() reference for input rules and capture behavior, and Testing for using it in tests.

Unknown commands

$ tool deploy
Error: Unknown command "deploy".
$ echo $?
1

The single-file tool example defines build, not deploy. Because tool has subcommands but no action of its own, an unrecognized name produces an error and exit code 1.

If the root has its own .action(), matching subcommands still take priority. Unmatched words are parsed as the root's positional arguments instead.

On this page