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:
| Approach | Use when |
|---|---|
type | A built-in conversion is enough |
parse | You need one small synchronous transformation of a string |
schema | A Standard Schema should own conversion, defaults, and validation |
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 1500msThe number type converts 1.5 before the Command Action runs, so flags.seconds is a number.
Built-in types
type | Action value |
|---|---|
string | string |
number | number |
boolean | boolean |
url | URL |
path | string, resolved against the working directory |
json | unknown, 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
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 regionparse 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
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 <=65535Put 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:
| Rule | Instead of | Write in the schema (Zod shown) |
|---|---|---|
| Text to number | type: "number" | z.coerce.number() |
| Default | default: 3000 | .default(3000) |
| Optional | leaving required unset | .optional() |
| Required | required: true | a schema that rejects undefined |
| Allowed values | choices: ["a", "b"] | z.enum(["a", "b"]) |
| Range or shape | parse 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 receivesstring | undefined, orstring[]withvariadic. - A flag keeps
type: "string"ortype: "boolean"so the parser knows whether a value token follows. The schema receivesstring | undefinedorboolean | undefined, or arrays withmultiple.
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 $?
1Conversion and parse failures use the PARSE code; schema failures use VALIDATION. See CrustError codes.