Modern Zod-based CLI builder, fully type-safe, super lightweight and flexible.
- 🎯 Full TypeScript support with robust type inference
- 🛡️ Runtime validation using Zod
- 🔄 Supports command aliases and nested commands
- 🌟 Global and command-specific options
- 📦 Shareable command presets
- 🎨 Flexible configuration API
- 🪶 Lightweight with zero runtime dependencies (except Zod)
npm install zodest zod
# or
yarn add zodest zod
# or
pnpm add zodest zod
# or
bun add zodest zodimport { z } from 'zod';
import { defineCommand, defineConfig, defineOptions } from 'zodest/config';
import { processConfig } from 'zodest';
const globalOptions = defineOptions(
z.object({
verbose: z.boolean().default(false),
}),
);
const config = defineConfig({
globalOptions,
commands: {
serve: defineCommand({
description: 'Start development server',
options: defineOptions(
z.object({
port: z.coerce.number().min(1024).default(3000),
}),
{ p: 'port' }, // aliases, type-safe with the schema
),
args: z.array(z.string()),
action(options, args) {
console.log('Server starting on port', options.port);
},
}),
},
});
// Process CLI arguments, returns the matching command
const result = processConfig(config, process.argv.slice(2));
console.log('result:', result._kind, result);
// => result._kind == 'serve', the matching command's name
// in `result` there's also `result.globalOptions`, and so on, all type-safe
// call the command
result.command.action(result.options, result.args);Commands are the basic building blocks. Each command can have:
- Description
- Options (with Zod validation)
- Arguments (with Zod validation)
- Aliases
- Action function
const myCommand = defineCommand({
description: 'My command description',
options: defineOptions(
z.object({
force: z.boolean().default(false),
}),
),
args: z.string().min(3),
aliases: ['mc', 'mycmd'],
action(options, args) {
// Fully typed options and args
},
});Shared options across all commands:
const globalOptions = defineOptions(
z.object({
debug: z.boolean().default(false),
config: z.string().optional(),
}),
{
d: 'debug', // aliases
c: 'config',
},
);Support for namespaced commands:
const config = defineConfig({
commands: {
'user add': defineCommand({...}),
'user remove': defineCommand({...}),
'user list': defineCommand({...}),
}
});Create reusable command sets:
import { withGlobalOptions } from 'zodest/config';
const userCommands = withGlobalOptions(globalOptions, (gopts) => ({
userAdd: defineCommand({...}),
userRemove: defineCommand({...}),
}));
// Use in any CLI
const config = defineConfig({
globalOptions,
commands: (gopts) => ({
...userCommands(gopts),
// Add more commands
})
});Creates a type-safe CLI configuration.
Defines a new command with options, arguments, and an action.
Creates a type-safe options definition with optional aliases.
Processes command line arguments against the configuration.
Creates a command preset with access to global options.
Returns all available commands from the configuration.
const command = defineCommand({
description: 'Complex command',
options: defineOptions(
z.object({
mode: z.enum(['development', 'production']),
port: z.coerce.number().min(1024),
}),
),
args: z.tuple([z.string(), z.coerce.number()]),
action(options, [path, num]) {
// path is string, num is number
},
});NOTE: mixing grouped commands with non-grouped ones breaks the defaultCommand inference of the
grouped commands, so you must use as any to mute TypeScript, or use a command from non-grouped
commands
const config = defineConfig({
defaultCommand: 'serve',
commands: {
serve: defineCommand({...})
dev: defineCommand({...})
}
});const devCommands = withGlobalOptions(globalOptions, (gopts) => ({
serve: defineCommand({...}),
build: defineCommand({...}),
}));
const dbCommands = withGlobalOptions(globalOptions, (gopts) => ({
'db migrate': defineCommand({...}),
'db seed': defineCommand({...}),
}));
const config = defineConfig({
globalOptions,
commands: (gopts) => ({
...devCommands(gopts),
...dbCommands(gopts),
})
});Apache-2.0