Crust logoCrust

Values and validation

Convert and validate values with built-in types, custom parsing, or Standard Schemas.

Command-line input starts as text. Each argument or flag definition picks one way to turn that text into a value:

ApproachUse when
typeA built-in conversion is enough
parseYou need one small synchronous transformation of a string
schemaA Standard Schema should own conversion, defaults, and validation
wait.ts
import { Crust } from "@crustjs/core";

const wait = new Crust("wait")
	.flags({ name: "seconds", type: "number", required: true })
	.action(({ flags, stdout }) => {
		const seconds = flags.seconds; // number
		stdout(`waiting ${seconds * 1000}ms`);
	});

await wait.execute();
$ wait --seconds 1.5
waiting 1500ms

The number type converts 1.5 before the Command Action runs, so flags.seconds is a number.

Built-in types

typeAction value
stringstring
numbernumber
booleanboolean
urlURL
pathstring, resolved against the working directory
jsonunknown, the parsed token

With a built-in type, Crust also handles presence: required rejects a missing value, default fills one in, and multiple or variadic collects several. See Arguments and Flags.

Custom parsing

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

const deploy = new Crust("deploy")
	.flags({ name: "regions", type: "string", parse: (raw) => raw.split(",") })
	.action(({ flags, stdout }) => {
		const regions = flags.regions; // string[] | undefined
		stdout(`deploying to ${regions?.join(" and ") ?? "the default region"}`);
	});

await deploy.execute();
$ deploy --regions us,eu
deploying to us and eu
$ deploy
deploying to the default region

parse runs once on the raw string and its return type becomes the action value, here string[]. It only converts. To reject a value, throw; the error is reported as Failed to parse --regions: <message>.

Schema-backed definitions

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

const Port = z.coerce.number().int().min(1).max(65535).default(3000);

const serve = new Crust("serve").args({ name: "port", schema: Port }).action(({ args, stdout }) => {
	const port = args.port; // number
	stdout(`listening on port ${port}`);
});

await serve.execute();
$ serve 8080
listening on port 8080
$ serve
listening on port 3000
$ serve 70000
Error: Invalid input:
  - args.port: Too big: expected number to be <=65535

Put a schema from any Standard Schema library (Zod, Valibot, ArkType) on the schema key. Crust hands the schema the raw input and gives the Command Action the schema's output type. Every issue is reported in one message.

The schema owns the rules

A schema definition cannot also set parse, choices, default, or required. Express those rules in the schema instead:

RuleInstead ofWrite in the schema (Zod shown)
Text to numbertype: "number"z.coerce.number()
Defaultdefault: 3000.default(3000)
Optionalleaving required unset.optional()
Requiredrequired: truea schema that rejects undefined
Allowed valueschoices: ["a", "b"]z.enum(["a", "b"])
Range or shapeparse plus a throw.int().min(1).max(65535)

Presence follows the same rule. An omitted input reaches the schema as undefined: .default(3000) substitutes the default, .optional() lets undefined through as number | undefined, and a schema without either rejects the omission.

What the schema receives

  • An argument omits type. The schema receives string | undefined, or string[] with variadic.
  • A flag keeps type: "string" or type: "boolean" so the parser knows whether a value token follows. The schema receives string | undefined or boolean | undefined, or arrays with multiple.

See ArgDef and FlagDef for the full contracts.

What gets checked, and when

At compile time

// @ts-expect-error parse must be synchronous
new Crust("fetch").args({ name: "remote", type: "string", parse: async (raw) => raw.trim() });

// A schema flag keeps `type` so the parser knows whether a value token follows
new Crust("serve").flags({ name: "port", type: "string", schema: Port });

// @ts-expect-error a schema argument owns conversion, so it cannot also set `type`
new Crust("serve").args({ name: "port", type: "string", schema: Port });

These lines continue serve.ts and only construct definitions. TypeScript rejects an asynchronous parse and a type on a schema argument, and accepts type: "string" on a schema flag; see the .args() and .flags() contracts.

TypeScript also infers the action value. For type or parse, it is the converted type, T | undefined unless required or default is set, and an array with multiple or variadic. For a schema, it is the schema's output type, which already reflects its defaults and optionality.

At runtime

Crust converts and validates input before the Command Action. A failed conversion, a thrown parse, or a schema issue prints an error, exits with 1, and skips the action:

$ wait --seconds soon
Error: Expected number for --seconds, got "soon"
$ echo $?
1
$ serve http
Error: Invalid input:
  - args.port: Invalid input: expected number, received NaN
$ echo $?
1

Conversion and parse failures use the PARSE code; schema failures use VALIDATION. See CrustError codes.

On this page