Validate
Standard Schema-based validation for CLI arguments, flags, prompts, and stores.
@crustjs/validate provides schema-first validation for Crust commands, prompts, and stores. It is built on the Standard Schema v1 spec — any schema library that implements this spec works with the prompt and store adapters out of the box.
For command validation, provider-specific entrypoints offer arg() / flag() helpers and commandValidator() middleware that derive CLI metadata (type, required, description, variadic) from the schema automatically — single source of truth.
Entrypoints
| Entrypoint | Command Middleware | Prompt & Store Adapters | Schema Libraries |
|---|---|---|---|
@crustjs/validate/zod | commandValidator | Yes | Zod 4 |
@crustjs/validate/effect | commandValidator | Yes | Effect Schema |
@crustjs/validate/standard | — | Yes | Any Standard Schema v1 library (Zod, Valibot, ArkType, etc.) |
The root entrypoint (@crustjs/validate) exports shared types only.
Install
bun add @crustjs/validateThen install the validator of your choice:
# Zod
bun add zod
# Effect
bun add effect@crustjs/core is a required peer dependency.
How It Works
- You declare args with
arg(name, schema)and flags withflag(schema, options)from a provider entrypoint. - Each helper introspects the schema to generate core-compatible
ArgDef/FlagDefwith hidden schema metadata attached via a symbol key. - You pass these definitions to
.args()and.flags()on theCrustbuilder, and usecommandValidator()as the.run()handler. - At runtime, after Crust parses argv, the middleware validates and transforms each value through its schema before calling your handler.
- Validation errors are collected and thrown as a single
CrustError("VALIDATION")with structureddetails.issues.
Transformation Behavior
Transformation support differs by target:
| Target / API | Validates | Applies schema transforms to returned value |
|---|---|---|
commandValidator | Yes | Yes |
promptValidator | Yes | No (returns true or error string only) |
parsePromptValue / parsePromptValueSync | Yes | Yes |
field / fieldSync (store fields) | Yes | No (validation-only adapter) |
So command middleware can transform handler inputs, and prompt parsing can transform parsed prompt values. Store field adapters only validate; they do not write transformed schema outputs back into store state.
Descriptions for help text are provided directly on the schema using the validator's native API (.describe() for Zod, .annotations() for Effect). The framework automatically extracts descriptions even through wrappers like .optional(), .default(), and .transform().
Zod Provider
Import from @crustjs/validate/zod.
Quick Example (Zod)
import { Crust } from "@crustjs/core";
import { arg, flag, commandValidator } from "@crustjs/validate/zod";
import { z } from "zod";
const greet = new Crust("greet")
.meta({ description: "Say hello" })
.args([arg("name", z.string().describe("Person to greet"))])
.flags({
loud: flag(z.boolean().default(false).describe("Shout the greeting"), {
short: "l",
}),
})
.run(commandValidator(({ args, flags }) => {
// args.name is string, flags.loud is boolean — fully typed
const msg = `Hello, ${args.name}!`;
console.log(flags.loud ? msg.toUpperCase() : msg);
}));
await greet.execute();Descriptions (Zod)
Use .describe() on the schema to provide help text. Descriptions are automatically resolved through wrappers:
// Description on the inner schema — resolved through .optional()
arg("name", z.string().describe("Person to greet").optional());
// Description on the inner schema — resolved through .default()
flag(z.number().describe("Port number").default(3000));
// Description on the inner schema — resolved through .transform()
flag(
z
.string()
.describe("Output format")
.transform((v) => v.toUpperCase()),
);Schema Support (Zod)
- Primitives:
z.string(),z.number(),z.boolean() - Enums and literals:
z.enum(["a", "b"]),z.literal("value") - Arrays:
z.array(z.string())(flags withmultiple: true) - Wrappers:
.optional(),.default(),.nullable(),.transform(),.pipe() - Descriptions:
z.string().describe("Help text")(auto-extracted for help output)
Schemas are introspected at definition time to determine CLI parser types (string, number, boolean) and optionality. Transforms and pipes are unwrapped to find the input type.
Variadic Args (Zod)
The last positional arg can be variadic, collecting all remaining positionals into an array:
const cmd = new Crust("rm")
.args([arg("files", z.string(), { variadic: true })])
.run(commandValidator(({ args }) => {
// args.files is string[]
for (const file of args.files) {
console.log(`Removing ${file}`);
}
}));Use a scalar schema (not z.array()). The framework handles array collection. Compile-time validation ensures variadic args are only in the last position.
Exports (Zod)
| Export | Kind | Description |
|---|---|---|
arg | function | Declare a named positional argument with a Zod schema |
flag | function | Declare a flag with a Zod schema and optional short/aliases |
commandValidator | function | Create a validated run handler via Zod schemas |
ZodArgDef<N, S, V, T> | type | ArgDef with hidden Zod schema metadata |
ZodFlagDef<S, A, T> | type | FlagDef with hidden Zod schema metadata |
CommandValidatorHandler<A, F> | type | Handler receiving ValidatedContext |
InferValidatedArgs<A> | type | Infer validated args from definitions |
InferValidatedFlags<F> | type | Infer validated flags from definitions |
Prompt and store adapters (promptValidator, parsePromptValue, parsePromptValueSync, field, fieldSync) are also re-exported — see Prompt Validation and Store Validation. Zod 4 implements Standard Schema natively, so no wrapping is needed.
Effect Provider
Import from @crustjs/validate/effect.
Constraint: Only context-free, synchronous schemas (
R = never) are supported in v1. Schemas that require an Effect context or use async combinators will produce a type error or throw at runtime.
Quick Example (Effect)
import { Crust } from "@crustjs/core";
import { arg, flag, commandValidator } from "@crustjs/validate/effect";
import { Schema } from "effect";
const greet = new Crust("greet")
.meta({ description: "Say hello" })
.args([
arg("name", Schema.String.annotations({ description: "Person to greet" })),
])
.flags({
loud: flag(
Schema.Boolean.annotations({ description: "Shout the greeting" }),
{ short: "l" },
),
})
.run(commandValidator(({ args, flags }) => {
// args.name is string, flags.loud is boolean — fully typed
const msg = `Hello, ${args.name}!`;
console.log(flags.loud ? msg.toUpperCase() : msg);
}));
await greet.execute();Descriptions (Effect)
Use .annotations({ description: "..." }) on the schema to provide help text. Descriptions are automatically resolved through wrappers:
// Description on the inner schema — resolved through UndefinedOr
arg(
"name",
Schema.UndefinedOr(
Schema.String.annotations({ description: "Person to greet" }),
),
);
// Description on the inner schema — resolved through transform
flag(
Schema.transform(
Schema.String.annotations({ description: "Output format" }),
Schema.String,
{ strict: false, decode: (v) => v.toUpperCase(), encode: (v) => v },
),
);Schema Support (Effect)
- Primitives:
Schema.String,Schema.Number,Schema.Boolean - Enums and literals:
Schema.Literal("a", "b"),Schema.Enums(MyEnum) - Arrays:
Schema.Array(Schema.String)(flags withmultiple: true) - Wrappers:
Schema.UndefinedOr(...),Schema.transform(...),Schema.TemplateLiteral - Descriptions:
schema.annotations({ description: "Help text" })(auto-extracted for help output)
Schemas are introspected via AST at definition time to determine CLI parser types (string, number, boolean) and optionality. Transformations and refinements are unwrapped to find the encoded input type.
Variadic Args (Effect)
The last positional arg can be variadic, collecting all remaining positionals into an array:
const cmd = new Crust("rm")
.args([arg("files", Schema.String, { variadic: true })])
.run(commandValidator(({ args }) => {
// args.files is string[]
for (const file of args.files) {
console.log(`Removing ${file}`);
}
}));Use a scalar schema (not Schema.Array()). The framework handles array collection. Compile-time validation ensures variadic args are only in the last position.
Exports (Effect)
| Export | Kind | Description |
|---|---|---|
arg | function | Declare a named positional argument with an Effect schema |
flag | function | Declare a flag with an Effect schema and optional short/aliases |
commandValidator | function | Create a validated run handler via Effect schemas |
EffectArgDef<N, S, V, T> | type | ArgDef with hidden Effect schema metadata |
EffectFlagDef<S, A, T> | type | FlagDef with hidden Effect schema metadata |
CommandValidatorHandler<A, F> | type | Handler receiving ValidatedContext |
InferValidatedArgs<A> | type | Infer validated args from definitions |
InferValidatedFlags<F> | type | Infer validated flags from definitions |
Prompt and store adapters are also re-exported — see Prompt Validation and Store Validation. Effect schemas must be wrapped with Schema.standardSchemaV1() before passing to these functions.
Prompt Validation
Validate prompt answers with schema-driven validation. These adapters accept any Standard Schema v1 compatible schema and return functions compatible with @crustjs/prompts.
Available from all three entrypoints: @crustjs/validate/zod, @crustjs/validate/effect, and @crustjs/validate/standard. Any Standard Schema library (Zod, Valibot, ArkType, etc.) works directly.
promptValidator(schema, options?)
Returns a function compatible with the validate option on prompts — returns true when valid, or a string error message when invalid.
import { promptValidator } from "@crustjs/validate/zod";
import { input } from "@crustjs/prompts";
import { z } from "zod";
const name = await input({
message: "Enter your name",
validate: promptValidator(z.string().min(1, "Name is required")),
});| Option | Type | Default | Description |
|---|---|---|---|
errorStrategy | "first" | "all" | "first" | Show first issue only, or all issues as a list |
promptValidator does validation only. It does not transform prompt values.
parsePromptValue(schema, value) / parsePromptValueSync
Parse a prompt answer through a schema and return the typed output. Useful for coercion (e.g. string to number). Throws CrustError("VALIDATION") on failure. The sync variant throws TypeError if the schema returns a Promise.
import { parsePromptValue } from "@crustjs/validate/zod";
import { input } from "@crustjs/prompts";
import { z } from "zod";
const raw = await input({ message: "Enter port" });
const port = await parsePromptValue(z.coerce.number().int().positive(), raw);
// port is typed as `number` — coerced from string inputEffect schemas must be wrapped with Schema.standardSchemaV1() before passing to prompt adapters.
Store Validation
Validate store fields with schema-driven validation. These adapters accept any Standard Schema v1 compatible schema and return functions for use with the per-field validate option in @crustjs/store.
Available from all three entrypoints: @crustjs/validate/zod, @crustjs/validate/effect, and @crustjs/validate/standard. Any Standard Schema library works directly.
field(schema) / fieldSync
Returns a validator function for use with a field's validate option. It returns void on success and throws on failure. The sync variant throws TypeError if the schema returns a Promise.
import { field } from "@crustjs/validate/zod";
import { createStore, configDir } from "@crustjs/store";
import { z } from "zod";
const store = createStore({
dirPath: configDir("my-cli"),
fields: {
theme: {
type: "string",
default: "light",
validate: field(z.enum(["light", "dark"])),
},
verbose: { type: "boolean", default: false },
},
});Effect schemas must be wrapped with Schema.standardSchemaV1() before passing to field adapters.
Store field adapters are validation-only. If your schema has transforms/coercions,
those transformed outputs are not written back into store state by field() / fieldSync().
Shared Behavior
These apply to both the Zod and Effect providers.
Strict Mode
When using commandValidator(), all args and flags in the command must be created with the matching provider's arg() / flag() helpers. Mixing plain core definitions causes a compile-time error (the handler parameter becomes never).
Plugin-injected flags (e.g. --help from helpPlugin) are silently skipped at runtime — they don't need schema metadata.
Compile-Time Validation
Both providers inherit the same compile-time checks as the Crust builder:
- Variadic position — only the last arg can be
{ variadic: true } - Flag alias collisions — aliases must not conflict with other flag names or aliases
These produce TypeScript errors at the call site if violated.
Error Handling
When validation fails, a CrustError("VALIDATION") is thrown with:
- A human-readable bullet-list message suitable for CLI output
- Structured
details.issuesarray of{ message, path }objects
commandValidator: validation failed
- args.name: Expected string, received number
- flags.port: Number must be greater than or equal to 1Exports
Shared Exports
The root entrypoint @crustjs/validate exports shared types used by all providers:
| Type | Description |
|---|---|
ValidatedContext<ArgsOut, FlagsOut> | Extended command context with validated args, flags, and input (pre-validation values) |
ValidationIssue | Normalized issue with message and dot-path path string |
Standard Entrypoint (@crustjs/validate/standard)
The @crustjs/validate/standard entrypoint provides prompt and store adapters that work with any Standard Schema v1 library — no command middleware. Use it when you want a provider-agnostic import, don't need commandValidator, or are using a schema library other than Zod or Effect (e.g. Valibot, ArkType).
| Export | Kind | Description |
|---|---|---|
promptValidator | function | Convert a Standard Schema into a prompt validate function |
parsePromptValue | function | Parse a prompt answer through a schema (async) |
parsePromptValueSync | function | Parse a prompt answer through a schema (sync) |
field | function | Convert a Standard Schema into an async field validator |
fieldSync | function | Convert a Standard Schema into a sync field validator |
StandardSchema | type | Standard Schema v1-compatible schema type alias |
InferInput<S> | type | Infer input type from a Standard Schema |
InferOutput<S> | type | Infer output type from a Standard Schema |
ValidationResult<T> | type | Discriminated result: { ok, value } or { ok: false, issues } |
PromptErrorStrategy | type | Error rendering: "first" or "all" |
PromptValidatorOptions | type | Options for promptValidator |
When to Use
Use @crustjs/validate/zod when:
- You want schemas as the single source of truth for parsing, validation, help text, and types
- You need transforms or coercion (e.g., parsing a string into a
Date) - You want fully inferred handler types without manual annotation
Use @crustjs/validate/effect when:
- You already use Effect and want schema-driven command validation
- You want Effect-style schema composition and parse errors
Use @crustjs/validate/standard when:
- You only need prompt or store validation (not command middleware)
- You use a Standard Schema library other than Zod or Effect (e.g. Valibot, ArkType)
- You want a provider-agnostic adapter
Use @crustjs/core directly when:
- You don't need runtime schema validation
- You want zero additional dependencies