Crust logoCrust

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

EntrypointCommand MiddlewarePrompt & Store AdaptersSchema Libraries
@crustjs/validate/zodcommandValidatorYesZod 4
@crustjs/validate/effectcommandValidatorYesEffect Schema
@crustjs/validate/standardYesAny Standard Schema v1 library (Zod, Valibot, ArkType, etc.)

The root entrypoint (@crustjs/validate) exports shared types only.

Install

bun add @crustjs/validate

Then install the validator of your choice:

# Zod
bun add zod

# Effect
bun add effect

@crustjs/core is a required peer dependency.

How It Works

  1. You declare args with arg(name, schema) and flags with flag(schema, options) from a provider entrypoint.
  2. Each helper introspects the schema to generate core-compatible ArgDef / FlagDef with hidden schema metadata attached via a symbol key.
  3. You pass these definitions to .args() and .flags() on the Crust builder, and use commandValidator() as the .run() handler.
  4. At runtime, after Crust parses argv, the middleware validates and transforms each value through its schema before calling your handler.
  5. Validation errors are collected and thrown as a single CrustError("VALIDATION") with structured details.issues.

Transformation Behavior

Transformation support differs by target:

Target / APIValidatesApplies schema transforms to returned value
commandValidatorYesYes
promptValidatorYesNo (returns true or error string only)
parsePromptValue / parsePromptValueSyncYesYes
field / fieldSync (store fields)YesNo (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 with multiple: 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)

ExportKindDescription
argfunctionDeclare a named positional argument with a Zod schema
flagfunctionDeclare a flag with a Zod schema and optional short/aliases
commandValidatorfunctionCreate a validated run handler via Zod schemas
ZodArgDef<N, S, V, T>typeArgDef with hidden Zod schema metadata
ZodFlagDef<S, A, T>typeFlagDef with hidden Zod schema metadata
CommandValidatorHandler<A, F>typeHandler receiving ValidatedContext
InferValidatedArgs<A>typeInfer validated args from definitions
InferValidatedFlags<F>typeInfer 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 with multiple: 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)

ExportKindDescription
argfunctionDeclare a named positional argument with an Effect schema
flagfunctionDeclare a flag with an Effect schema and optional short/aliases
commandValidatorfunctionCreate a validated run handler via Effect schemas
EffectArgDef<N, S, V, T>typeArgDef with hidden Effect schema metadata
EffectFlagDef<S, A, T>typeFlagDef with hidden Effect schema metadata
CommandValidatorHandler<A, F>typeHandler receiving ValidatedContext
InferValidatedArgs<A>typeInfer validated args from definitions
InferValidatedFlags<F>typeInfer 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")),
});
OptionTypeDefaultDescription
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 input

Effect 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.issues array of { message, path } objects
commandValidator: validation failed
  - args.name: Expected string, received number
  - flags.port: Number must be greater than or equal to 1

Exports

Shared Exports

The root entrypoint @crustjs/validate exports shared types used by all providers:

TypeDescription
ValidatedContext<ArgsOut, FlagsOut>Extended command context with validated args, flags, and input (pre-validation values)
ValidationIssueNormalized 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).

ExportKindDescription
promptValidatorfunctionConvert a Standard Schema into a prompt validate function
parsePromptValuefunctionParse a prompt answer through a schema (async)
parsePromptValueSyncfunctionParse a prompt answer through a schema (sync)
fieldfunctionConvert a Standard Schema into an async field validator
fieldSyncfunctionConvert a Standard Schema into a sync field validator
StandardSchematypeStandard Schema v1-compatible schema type alias
InferInput<S>typeInfer input type from a Standard Schema
InferOutput<S>typeInfer output type from a Standard Schema
ValidationResult<T>typeDiscriminated result: { ok, value } or { ok: false, issues }
PromptErrorStrategytypeError rendering: "first" or "all"
PromptValidatorOptionstypeOptions 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

On this page