Crust logoCrust

Arguments and Flags

Choose and define typed positional and named command inputs.

Use a positional argument for the thing a command acts on, and a flag for how it should act.

import { Crust } from "@crustjs/core";

const app = new Crust("convert")
	.args({ name: "input", type: "string", required: true })
	.flags({ name: "format", type: "string", default: "html" })
	.action(({ , , stdout }) => {
		stdout(`Converting ${args.input} as ${flags.format}`);
	});

await app.execute();
$ convert report.md --format markdown
Converting report.md as markdown

The Command Action receives both values under the names in their definitions.

Arguments

import { Crust } from "@crustjs/core";

const convert = new Crust("convert")
	.args(
		{ name: "input", type: "string", required: true },
		{ name: "format", type: "string", default: "json" },
		{ name: "label", type: "string" },
	)
	.action(({ args, stdout }) => {
		const  = args.input;
		const  = args.format;
		const  = args.label;
		stdout(`${input} -> ${format}${label ? ` (${label})` : ""}`);
	});

await convert.execute();
$ convert report.md
report.md -> json
$ convert report.md yaml draft
report.md -> yaml (draft)

Positional values fill definitions in order. Set required: true when omission should fail, use default for a fallback, and leave both out for an optional value. These definitions infer string, string, and string | undefined in the Command Action; see ArgDef for the full definition.

Variadic arguments

import { Crust } from "@crustjs/core";

const copy = new Crust("copy")
	.args(
		{ name: "destination", type: "path", required: true },
		{ name: "files", type: "path", variadic: true },
	)
	.action(({ args, stdout }) => {
		const  = args.files;
		stdout(`${files.length} files to ${args.destination}`);
	});

await copy.execute();
$ copy dist a.ts b.ts
2 files to /home/me/project/dist

Use a final variadic argument when the command accepts any number of values in that position. The Command Action receives those values as an array; type: "path" resolves each value against the working directory.

Choices

import { Crust } from "@crustjs/core";

const run = new Crust("run")
	.args({ name: "runtime", type: "string", choices: ["bun", "node"] })
	.action(({ args, stdout }) => {
		const  = args.runtime;
		stdout(`runtime: ${runtime}`);
	});

await run.execute();
$ run deno
Error: Invalid value "deno" for <runtime>. Expected one of: bun, node

Use choices when the accepted strings are known in advance. The Command Action gets their string union, plus undefined here because the argument is neither required nor defaulted, and an invalid value fails before the action runs.

Pass through raw tokens

import { Crust } from "@crustjs/core";

const wrap = new Crust("wrap").action(({ rawArgs, stdout }) => stdout(rawArgs.join(" ")));

await wrap.execute();
$ wrap -- --watch src
--watch src

Tokens after -- bypass argument and flag parsing and reach the Command Action as rawArgs. Use this for wrapper commands that pass options to another tool.

Flags

Use boolean flags for switches and value-taking flags for named settings.

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

const serve = new Crust("serve")
	.flags({ name: "color", type: "boolean" }, { name: "port", type: "number" })
	.action(({ , stdout }) => {
		stdout(`color=${String(flags.color)} port=${String(flags.port)}`);
	});

await serve.execute();
$ serve --color --port 3000
color=true port=3000
$ serve
color=undefined port=undefined

The definitions use name: "color" and name: "port" without dashes. A user types --color --port 3000, and the action reads flags.color and the converted number in flags.port.

In this example, neither flag has required, default, or an environment binding, so omitting it gives an action value of undefined. When supplied, a value-taking flag needs one value, so serve --port fails. Built-in types convert CLI text and reject invalid input before the action runs:

$ serve --port nope
Error: Expected number for --port, got "nope"

Required and defaulted values

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

const publish = new Crust("publish")
	.flags(
		{ name: "token", type: "string", required: true },
		{ name: "registry", type: "string", default: "npm" },
	)
	.action(({ , stdout }) => {
		stdout(`publishing to ${flags.registry}`);
	});

await publish.execute();

Use default when omission should choose a value. This success case omits --registry, so the action receives its npm default. The action does not print the token:

$ publish --token example
publishing to npm

Set required: true when a value must be supplied. This example has no environment binding, so a missing required flag fails before the action runs:

$ publish
Error: Missing required flag "--token"
$ echo $?
1

Both defaulted and required values are non-optional in the action.

Aliases and negation

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

const serve = new Crust("serve")
	.flags({ name: "color", type: "boolean", short: "c", aliases: ["colour"] })
	.action(({ flags, stdout }) => {
		stdout(`color=${String(flags.color)}`);
	});

await serve.execute();

Set short for a one-character alias and aliases for long aliases. Every spelling writes to the property named by name:

$ serve --colour
color=true
$ serve -c
color=true

Boolean flags also accept --no-<name> and --no-<alias> unless noNegate: true:

$ serve --no-colour
color=false

See the .flags() contract for all accepted spellings.

Repeated values

src/serve-values.ts
import { Crust } from "@crustjs/core";

const serve = new Crust("serve")
	.flags(
		{ name: "target", type: "string", multiple: true, short: "t" },
		{ name: "runtime", type: "string", choices: ["bun", "node"], default: "bun" },
		{ name: "tag", type: "string" },
	)
	.action(({ , stdout }) => {
		stdout(
			`targets=${flags.target?.join(",") ?? "undefined"} runtime=${flags.runtime} tag=${String(flags.tag)}`,
		);
	});

await serve.execute();

Set multiple: true when a flag may occur more than once. Without a delimiter, each occurrence contributes one value in order:

$ serve --target linux -t darwin
targets=linux,darwin runtime=bun tag=undefined

Without a default or environment binding, omitting a repeated flag gives undefined. Repeat the flag for every value, or configure a delimiter. In --target linux darwin, darwin is a positional argument, not another target.

Flag choices

Use choices for a fixed set of strings. An accepted value reaches the action:

$ serve --runtime node
targets=undefined runtime=node tag=undefined

An invalid value fails before the action runs:

$ serve --runtime deno
Error: Invalid value "deno" for --runtime. Expected one of: bun, node
$ echo $?
1

Environment variables

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

const publish = new Crust("publish")
	.flags(
		{ name: "token", type: "string", env: { name: "PUBLISH_TOKEN" }, required: true },
		{ name: "registry", type: "string", env: { name: "PUBLISH_REGISTRY" }, default: "npm" },
		{
			name: "tag",
			type: "string",
			multiple: true,
			delimiter: ",",
			env: { name: "PUBLISH_TAGS", delimiter: ":" },
		},
	)
	.action(({ flags, stdout }) => {
		stdout(`publishing to ${flags.registry} tags=${flags.tag?.join(",") ?? "undefined"}`);
	});

await publish.execute();

Set env: { name: "VARIABLE" } to read a flag from that environment variable when it is absent from argv. Precedence is argv, then env, then default; an environment value also satisfies required. Environment bindings are declarations: variables are read during terminal invocation, not when defining the flag.

$ PUBLISH_TOKEN=example publish
publishing to npm tags=undefined
$ PUBLISH_TOKEN=example PUBLISH_REGISTRY=github publish --registry jsr
publishing to jsr tags=undefined

After its own optional splitting, the environment value enters the same conversion and validation path as argv, so type conversion, choices, parse, and schemas apply unchanged. Boolean flags accept true or 1 as true; any other value is false, and a noNegate flag rejects a false value the way it rejects --no-<name>.

The two delimiters are independent: flag-level delimiter splits string argv occurrences, while env.delimiter splits environment text. Both require multiple: true, use nonempty literal separators, and drop empty segments. Neither inherits the other's setting; without env.delimiter, the entire environment value is one occurrence. Boolean argv switches remain --flag / --no-flag (not --flag=true,false); repeated booleans can use env: { name: "FEATURES", delimiter: "," } for environment text such as true,false.

The example above uses commas on argv and colons in PUBLISH_TAGS:

$ PUBLISH_TOKEN=example PUBLISH_TAGS=latest::beta publish
publishing to npm tags=latest,beta
$ PUBLISH_TOKEN=example publish --tag latest,beta --tag next
publishing to npm tags=latest,beta,next

An explicit argv occurrence always wins over the environment, even when it splits to zero occurrences. Invalid argv values fail rather than trying env; invalid environment values fail rather than trying the default. Zero occurrences after splitting follow the existing default/required or schema rules. Help, man pages, and generated skills list the variable as [env: PUBLISH_TOKEN] and never print its value. Programmatic run() binds structured input directly: it does not read the environment and never splits arrays. Positional arguments have no env; use a flag.

Values that start with -

For a value-taking flag, attach a dash-leading value with = so it is not mistaken for another flag:

$ serve --tag=-nightly
targets=undefined runtime=bun tag=-nightly

Reuse across commands

src/tools.ts
import { Crust, defineCommand, defineFlag } from "@crustjs/core";

const format = defineFlag("format", { type: "string", default: "json" });
const print = defineCommand("print", (command) =>
	command.flags(format).action(({ flags, stdout }) => stdout(`print as ${flags.format}`)),
);
const inspect = defineCommand("inspect", (command) =>
	command.flags(format).action(({ flags, stdout }) => stdout(`inspect as ${flags.format}`)),
);

await new Crust("tools").add(print, inspect).execute();

Use defineFlag() when several commands need the same definition. Attach it to every command that should parse the flag. Both commands above consume format:

$ tools print --format yaml
print as yaml
$ tools inspect
inspect as json

Validation

Built-in conversion covers common values such as the number flag above. For custom parsers and Standard Schemas, see Values and validation.

On this page