Bunli
API Reference

option

Create command options with schema validation

Creates a command option with schema validation and metadata.

Syntax

function option<S extends StandardSchemaV1>(
  schema: S,
  metadata?: {
    short?: string;
    description?: string;
    repeatable?: boolean;
    argumentKind?: "flag" | "value";
  },
): CLIOption<S>;

Parameters

schema

A Standard Schema v1 compatible schema (Zod, Valibot, etc.) that validates and transforms the option value.

metadata (optional)

Additional metadata for the option:

  • short - Short alias for the option (string, e.g. -e)
  • description - Help text shown in --help output
  • repeatable - Collect repeated flag occurrences and validate them as an array
  • argumentKind - Controls whether the option takes a value ('value', the default) or is a boolean flag ('flag') that doesn't consume a following argument

Returns

A CLIOption object that can be used in command definitions.

Examples

Basic Options

import { option } from "@bunli/core";
import { z } from "zod";

// String option with default
const name = option(z.string().default("world"), { description: "Name to greet" });

// Number with validation
const port = option(z.coerce.number().int().min(1).max(65535).default(3000), {
  short: "p",
  description: "Port number",
});

// Boolean flag
const verbose = option(z.coerce.boolean().default(false), {
  short: "v",
  description: "Enable verbose output",
});

// Enum option
const env = option(z.enum(["dev", "staging", "prod"]), {
  short: "e",
  description: "Target environment",
});

Using in Commands

import { defineCommand, option } from "@bunli/core";
import { z } from "zod";

export default defineCommand({
  name: "serve",
  description: "Start the server",
  options: {
    port: option(z.coerce.number().default(3000), { short: "p", description: "Port to listen on" }),
    host: option(z.string().default("localhost"), { short: "h", description: "Host to bind to" }),
    secure: option(z.coerce.boolean().default(false), { short: "s", description: "Use HTTPS" }),
  },
  handler: async ({ flags }) => {
    // flags.port: number
    // flags.host: string
    // flags.secure: boolean
  },
});

Schema Types

Coercion

Use z.coerce for automatic type conversion from command-line strings:

// Coerce string to number
option(z.coerce.number()); // "123" → 123

// Coerce string to boolean
option(z.coerce.boolean()); // "true" → true, "false" → false

// Coerce string to date
option(z.coerce.date()); // "2024-01-01" → Date object

Transformations

Transform input values with custom logic:

// Parse JSON
const config = option(
  z.string().transform((val) => JSON.parse(val)),
  { description: "JSON configuration" },
);

// Convert to uppercase
const env = option(
  z.string().transform((val) => val.toUpperCase()),
  { description: "Environment name" },
);

// Parse comma-separated values
const tags = option(
  z.string().transform((val) => val.split(",")),
  { description: "Comma-separated tags" },
);

// Parse file size with units
const size = option(
  z
    .string()
    .regex(/^\d+[kmg]b?$/i)
    .transform((val) => {
      const match = val.match(/^(\d+)([kmg])b?$/i)!;
      const [, num, unit] = match;
      const multipliers = { k: 1024, m: 1024 ** 2, g: 1024 ** 3 };
      return parseInt(num) * multipliers[unit.toLowerCase()];
    }),
  { description: "Size limit (e.g., 512k, 1g)" },
);

Complex Schemas

// Object validation
const server = option(
  z
    .string()
    .transform((val) => JSON.parse(val))
    .pipe(
      z.object({
        host: z.string(),
        port: z.number(),
        secure: z.boolean().optional(),
      }),
    ),
  { description: "Server config as JSON" },
);

// Array with validation
const ignore = option(
  z
    .string()
    .transform((val) => val.split(","))
    .pipe(z.array(z.string().min(1))),
  { description: "Comma-separated ignore patterns" },
);

// Union types
const output = option(
  z.union([z.literal("json"), z.literal("yaml"), z.literal("toml")]).default("json"),
  { description: "Output format" },
);

Validation

Schemas are validated automatically when commands run:

const port = option(
  z.coerce
    .number()
    .int("Port must be an integer")
    .min(1, "Port must be at least 1")
    .max(65535, "Port must be at most 65535"),
  { short: "p", description: "Port number" },
);

// $ my-cli serve --port abc
// Validation errors:
//   --port:
//     • Expected number, received nan

// $ my-cli serve --port 0
// Validation errors:
//   --port:
//     • Port must be at least 1

// $ my-cli serve --port 80000
// Validation errors:
//   --port:
//     • Port must be at most 65535

Optional vs Required

// Required option (no default)
const input = option(z.string(), { description: "Input file" });

// Optional with default
const output = option(z.string().default("./output"), { description: "Output directory" });

// Truly optional (can be undefined)
const config = option(z.string().optional(), { description: "Config file path" });

// Nullable option
const template = option(z.string().nullable().default(null), { description: "Template name" });

Standard Schema Support

The option function accepts any Standard Schema v1 compatible schema:

// Using Zod
import { z } from "zod";
option(z.string());

// Using Valibot
import * as v from "valibot";
option(v.pipe(v.string(), v.minLength(1)));

// Any Standard Schema v1 library
import { schema } from "any-standard-schema-lib";
option(schema.string());

All options must have a schema. There are no "raw" options in Bunli - this ensures type safety and consistent validation across your CLI.

Best Practices

  1. Use descriptive names - Option names should clearly indicate their purpose
  2. Add descriptions - Always include helpful description text
  3. Use short flags wisely - Reserve single letters for commonly used options
  4. Provide defaults - Make options easier to use with sensible defaults
  5. Use coerce for CLI inputs - z.coerce handles string-to-type conversion
  6. Validate constraints - Add .min(), .max(), .regex() etc. for validation
  7. Transform when needed - Parse complex inputs with .transform()

Common Patterns

File Paths

const file = option(
  z.string().refine(
    (val) => existsSync(val),
    (val) => ({ message: `File not found: ${val}` }),
  ),
  { description: "Input file path" },
);

URLs

const url = option(z.string().url("Must be a valid URL"), { description: "API endpoint" });

Environment Variables

const apiKey = option(z.string().default(process.env.API_KEY || ""), {
  description: "API key (or set API_KEY env var)",
});

Multiple Values

// From repeated flags: --tag foo --tag bar
const tags = option(z.array(z.string()).default([]), {
  description: "Tags (can be used multiple times)",
  repeatable: true,
});

// From comma-separated: --tags foo,bar,baz
const tags = option(
  z
    .string()
    .transform((val) => val.split(",").map((s) => s.trim()))
    .pipe(z.array(z.string())),
  { description: "Comma-separated tags" },
);

See Also

On this page