Introduction
A type-safe CLI parser built on Zod. Declare commands, options and positional args as schemas and get a fully-typed, validated result.
zodline turns Zod schemas into a command-line interface. You declare what your CLI accepts — commands,
options, positional arguments — and zodline parses process.argv, validates it against those schemas, and
hands back a fully-typed result. The types come from the schemas you already wrote; there are no generics
to thread through by hand.
It has zero runtime dependencies — Zod is the only peer dependency — and ships as ESM only.
Why zodline
Types from schemas
options and args are inferred from your Zod schemas. Rename a field and every call site fails to
compile.
Declarative
A command is an object: a description, an options schema, an args schema, an action. No builder chains.
Validation included
Coercion, defaults, refinements, unions — anything Zod can express, your CLI can accept.
Strict by default
An unknown flag is an error, not a silently ignored key. Typos surface immediately.
Zero dependencies
Nothing but Zod, which you already have. Nothing to audit, nothing to bloat your install.
Parse, then run
processConfig validates and returns. It never invokes your action, so commands stay testable.
A first look
import { z } from 'zod';
import { defineCommand, defineConfig, defineOptions, processConfig } from 'zodline';
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);
},
});
const config = defineConfig({
meta: { name: 'my-cli', version: '1.0.0' },
commands: { greet },
});
const result = processConfig(config, process.argv.slice(2));
await result.command.action(result.options, result.args);
my-cli greet --name World --loud
# HELLO, WORLD!
options above is typed as { name: string; loud: boolean } — inferred from the schema, not declared twice.
Parsing and execution are separate
processConfig returns { command, options, args }. It does not call command.action. Running the
command is one line you write yourself, which means you can parse an argv array in a test and assert on the
result without executing anything.
const result = processConfig(config, ['greet', '--name', 'World']);
expect(result.options).toEqual({ name: 'World', loud: false });
See Parsing vs. execution for the full rationale.