Quickstart
Build a CLI with two commands, typed options and positional arguments, then run it.
This walks through a small my-cli with two commands: greet, which takes options, and copy, which takes
positional arguments.
Install the packages
npm install zodline zodpnpm add zodline zodyarn add zodline zodbun add zodline zodDefine a command
A command bundles a description, an options schema, and the action that runs it. defineOptions takes the
schema and an optional alias map from short flag to schema key.
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);
},
});options inside action is typed { name: string; loud: boolean }. The .describe() text becomes the
option’s help line.
Add positional arguments
Positional arguments are validated by a single schema that receives the whole array. A z.tuple gives you a
fixed shape with per-position names.
import { z } from 'zod';
import { defineCommand, defineOptions } from 'zodline';
export const copy = defineCommand({
description: 'Copy a file to another location',
args: z.tuple([z.string().describe('Source file'), z.string().describe('Destination file')]),
options: defineOptions(
z.object({
verbose: z.boolean().default(false).describe('Show detailed output'),
}),
{ v: 'verbose' },
),
action: async (options, args) => {
const [source, destination] = args;
if (options.verbose) {
console.log(`Copying ${source} to ${destination}...`);
}
console.log(`Copied ${source} to ${destination}`);
},
});args is typed [string, string], so destructuring it is safe.
Assemble the config
meta drives the generated help screen and the --version flag.
import { defineConfig } from 'zodline';
import { copy } from './commands/copy.js';
import { greet } from './commands/greet.js';
export const config = defineConfig({
meta: {
name: 'my-cli',
version: '1.0.0',
description: 'A simple example CLI',
},
commands: { greet, copy },
});Parse and run
processConfig validates argv and returns the matched command. Running it is your call — see
Parsing vs. execution.
#!/usr/bin/env node
import { processConfig } from 'zodline';
import { config } from './config.js';
try {
const result = processConfig(config, process.argv.slice(2));
await result.command.action(result.options, result.args);
} catch (error) {
console.error('Error:', error.message);
process.exit(1);
}Try it
my-cli greet --name World
# Hello, World!
my-cli greet -n World -l
# HELLO, WORLD!
my-cli copy src.txt dest.txt --verbose
# Copying src.txt to dest.txt...
# Copied src.txt to dest.txtHelp and version come for free:
my-cli --help # lists every command
my-cli greet --help # lists greet's options, aliases and defaults
my-cli --version # 1.0.0What you get for free
Flag syntax
--flag, -f, --flag=value, --flag value, clustering, repeated flags.
kebab-case mapping
--dry-run fills the schema key dryRun without extra configuration.
Generated help
Built from your schemas, descriptions and defaults.
Strict options
Unknown flags throw a ZodlineError instead of being ignored.