Type-safe CLIs.
Declared as schemas.
Declare your commands, options, and positional arguments as Zod schemas. zodline parsesprocess.argv, validates it against them, and returns a fully typed result.
import { z } from 'zod';
import { defineCommand, defineOptions } from 'zodline';
export const greet = defineCommand({
description: 'Greet someone',
options: defineOptions(
z.object({
name: z.string().describe('Name to greet'),
loud: z.boolean().default(false).describe('Use uppercase'),
}),
{ n: 'name', l: 'loud' },
),
action: async (options) => {
const greeting = `Hello, ${options.name}!`;
console.log(options.loud ? greeting.toUpperCase() : greeting);
},
});Requirements
Type-safe
Option and argument types come straight from your Zod schemas. No manual generics.
Zero runtime dependencies
Zod is the only peer dependency. Nothing else is installed alongside it.
Flexible flag parsing
--flag, -f, --flag=value, --flag value, and short-flag clustering like -abc.
Kebab to camel
Pass --android-max and the schema key androidMax receives the value.
Aliases
Map any short flag to a schema key with the alias record on defineOptions.
Array normalization
A single value for an array field is wrapped in an array before validation.
Generated help
--help and --version are built from your schemas and their .describe() text.
Strict by default
An unknown option raises a ZodlineError instead of being silently dropped.
Parsing and execution stay separate
processConfig returns { command, options, args }and stops there. Running the action is your call, which leaves every action a plain function over typed values: a test invokes it with an object literal, no argv and no process involved.Positional arguments arrive as the second parameter, validated by the command's args schema.
// processConfig parses and validates. It never runs your action.
const result = processConfig(config, process.argv.slice(2));
await result.command.action(result.options, result.args);
// Which leaves the action a plain function a test can call directly.
test('greet shouts when loud is set', async () => {
await greet.action({ name: 'World', loud: true }, undefined);
expect(stdout).toContain('HELLO, WORLD!');
});Every flag form, normalized
zodline accepts the forms people actually type and resolves them to your schema keys before validation runs.
| You type | Schema field | options receives |
|---|---|---|
| --name World | name: z.string() | { name: 'World' } |
| --name=World | name: z.string() | { name: 'World' } |
| -n World | alias { n: 'name' } | { name: 'World' } |
| --loud | loud: z.boolean() | { loud: true } |
| -abc | three boolean fields | { a: true, b: true, c: true } |
| --tag x | tag: z.array(z.string()) | { tag: ['x'] } |
| --tag x --tag y | tag: z.array(z.string()) | { tag: ['x', 'y'] } |
| --android-max 34 | androidMax: z.string() | { androidMax: '34' } |
Start here
Installation
Install zodline and its Zod peer dependency.
Quickstart
From an empty file to a CLI with two working commands.
Commands
Descriptions, actions, and the default command.
Options
Schemas, aliases, defaults, and unknown-option errors.
API reference
Every export, with its full signature.
Error handling
Which failures throw what, and how to report them.