Type-safe CLI framework powered by Zod schemas
Define your CLI with Zod schemas. Get type safety, validation, help generation, interactive prompts, shell completions, AI tool integration, and more — all from a single source of truth.
Built on Standard Schema, so it also works with Valibot, ArkType, and others.
npm install padrone zodThe fastest way to get started is with padrone init:
npx padrone init my-cli
cd my-cli && bun i && bun devimport { createPadrone } from 'padrone';
import * as z from 'zod/v4';
const program = createPadrone('myapp')
.command('greet', (c) =>
c
.arguments(
z.object({
names: z.array(z.string()).describe('Names to greet'),
prefix: z.string().optional().describe('Prefix').meta({ flags: 'p' }),
}),
{ positional: ['...names'] },
)
.action((args) => {
for (const name of args.names) {
console.log(`Hello, ${args.prefix ?? ''} ${name}!`);
}
}),
);
program.cli();myapp greet John Jane -p Mr.
# Hello, Mr. John!
# Hello, Mr. Jane!// Multiple ways to run commands
program.cli(); // from process.argv
program.eval('greet John --prefix Mr.'); // from a string
program.run('greet', { names: ['John'], prefix: 'Mr.' }); // typed args, schema defaults applied
program.api().greet({ names: ['John'], prefix: 'Mr.' }); // as a function (returns the result, throws on errors)
// Parse without executing
const { args } = program.parse('greet John --prefix Mr.');
// Interactive REPL
for await (const result of program.repl()) { /* ... */ }
// AI tool for Vercel AI SDK
const tool = program.tool();
// MCP server for AI assistants (Claude, Cursor, etc.) [experimental]
await program.mcp(); // or: myapp mcp
// REST server with OpenAPI docs [experimental]
await program.serve(); // or: myapp serve
// Shell completions
const script = program.completion('zsh');
// Help in multiple formats
program.help('greet'); // text
program.help('greet', { format: 'json' }); // json, markdown, html, ansiArguments — positional args, variadic args, short flags (-v), long aliases (--dry-run), auto kebab-case aliases, negatable booleans (--no-verbose), custom negation keywords (--remote → sets local to false).
Env & Config — load from environment variables with .extend(padroneEnv(schema)) (or padroneEnv({ prefix: 'MY_APP' }) for every option, MY_APP_DB__HOST for nested ones, MY_APP_TAGS=a,b for arrays, scope: 'command' for MY_APP_LIST_LIMIT) and config files with .extend(padroneConfig({ files, schema })) (a script config can export a function, typed with defineConfig(); $production: { ... } overrides apply by NODE_ENV), with --profile profiles (profiles: true), per-command sections (serve: { port: 3000 }, on by default) and a config get|set|list|edit command (command: true, with --local/--file, keeping comments). Precedence: CLI > stdin > env > config > defaults.
Interactive prompts — auto-prompt for missing fields. Booleans become confirm, enums become select, arrays become multi-select. Actions ask their own questions with ctx.prompt.text/confirm/select/multiselect/password/group, which fail fast (or take their default) without a terminal.
Progress indicators — auto-managed spinners and progress bars with elapsed time and ETA. .extend(padroneProgress({ message: 'Deploying...', bar: true, time: true, eta: true })).
Extension-first architecture — most built-in features (help, version, REPL, color, signal handling, auto-output, stdin, config, interactive, suggestions) are implemented as extensions composed via .extend(). Any built-in can be disabled or replaced.
Interceptors — middleware hooks for 7 phases (start, parse, route, validate, execute, error, shutdown). Onion model with next(). Extensions register interceptors under the hood. Create your own with defineInterceptor().
Composition — mount programs as subcommands with .mount(), override commands with merge semantics.
Wrapping (experimental) — wrap external CLI tools with .wrap({ command: 'git', args: ['commit'] }).
| Method | What it does |
|---|---|
.arguments(schema, meta?) |
Define args with Zod schema, positional config, field metadata |
.globalArgs(schema, meta?) |
Define options shared by a command and all its subcommands, merged into their args |
.action(handler) |
Set handler (args, ctx, base?) => result; ctx.run(name, args) runs another command and resolves to its result |
.dryRun(handler) |
Add --dry-run / -n: runs handler instead of the action and prints what would change |
.hook('preAction' | 'postAction', handler) |
Run code before / after the action of the command and all its subcommands (ancestors' pre-hooks first, post-hooks last) |
.command(name, builder) |
Add subcommand (name or [name, ...aliases]); defineCommand((c) => ...) types a builder kept in its own file (defineCommand<Context>()((c) => ...) with the program's context, defineCommand<Context, typeof globals>() with its global args too; defineCommand().requires<T>() for context an interceptor provides) |
.describe(text) |
Set the description shown in help (shorthand for .configure({ description })) |
.context(transform?) |
Define typed context or transform inherited context |
.mount(name, program, options?) |
Mount another program as subcommand tree |
.configure(config) |
Set title, description, version, help customization (help: { usage, before, after } or (info, ctx) => …), etc. |
.extend(padroneEnv(schema)) |
Map env vars to args (composable extension) |
.extend(padroneConfig({ files, schema })) |
Load args from config files, optionally layered (merge, extends), with profiles and a config command (command) (composable extension) |
.wrap(config) |
Wrap an external CLI tool (experimental) |
.extend(padroneProgress(config?)) |
Auto-managed progress indicator and progress.tasks() task lists (extension) |
.extend(padroneJson()) |
--json flag: results and errors as JSON, --jq (a lazy jq subset with string interpolation, @sh, range/limit, paths and a step budget) / --template to filter and format, fields for --json name,url (extension) |
.extend(padroneFormat()) |
--output/-o: text, json, yaml, csv, tsv or table, with --columns / --sort / --no-header, columns labels, pipedTable: 'tsv', csvLineEnding: 'crlf', sanitize and csvFormulaEscape for untrusted data (extension) |
.extend(padroneConfirm()) |
Confirm mutation: true commands (or .configure({ confirm })), skipped with --yes or <PROGRAM>_YES=1; nonInteractive decides without a terminal (extension) |
.extend(padroneCredentials()) |
Store secrets in the OS keychain (macOS security, Linux secret-tool) or a 0600 file: ctx.context.credentials.get/set/delete (extension) |
.intercept(interceptor) |
Register middleware interceptor (use defineInterceptor()) |
.extend(...extensions) |
Apply build-time extensions in order (bundles of config, commands, interceptors): .extend(padroneJson(), padroneFormat()) |
.runtime(runtime) |
Custom I/O (for non-terminal use) |
.extend(padroneUpdateCheck(config?)) |
Background version check (extension) |
.extend(padroneUpgrade(options?)) |
upgrade self-update command (extension) |
.extend(padroneAliases(options?)) |
User-defined command aliases with $1/$@ placeholders, shared with alias import/export (extension) |
.extend(padroneResponseFiles(options?)) |
@file arguments expand into the file's arguments (extension) |
.extend(padroneExternalCommands(options?)) |
External subcommands: my-cli foo runs my-cli-foo from PATH (extension) |
.extend(padronePlugins(options?)) |
Plugins users install at runtime (plugins install|uninstall|list|link), loaded at startup (extension) |
.async() |
Mark as async validation |
| Method | What it does |
|---|---|
.cli(prefs?) |
Entry point — parses process.argv, throws on errors. Pass context in prefs. |
.eval(input, prefs?) |
Parse + validate + execute string, returns errors softly. Pass context in prefs. |
.run(command, args?, prefs?) |
Run by name with typed args: checked against the schema (defaults applied), without the parse/validate phases or printing. An async action's result is awaited, as in .eval(). Args can be left out (or undefined / {}) when none are required. Pass context in prefs (required when the program declares one). |
.parse(input?) |
Parse without executing |
.api(prefs?) |
Commands as typed functions that return the result and throw on invalid args or a failing action |
.repl(options?) |
Interactive REPL session |
.help(command?, prefs?) |
Generate help (text, ansi, markdown, html, json) |
.tool(prefs?) |
Vercel AI SDK tool definition. Pass context in prefs (as for .serve() / .mcp()). |
.mcp(prefs?) |
Start MCP server (HTTP or stdio) (experimental) |
.serve(prefs?) |
Start REST server with OpenAPI docs (experimental) |
.completion(shell?) |
Shell completion script |
.find(command) |
Look up command by path |
.stringify(command?, args?) |
Convert back to CLI string |
| Field | Example | Purpose |
|---|---|---|
description |
'Output file' |
Help text (same as .describe()) |
flags |
'v' |
Single-char short flag (-v) |
alias |
'dry-run' |
Multi-char long alias (--dry-run) |
negative |
'remote' |
Custom negation keyword for booleans (disables --no-) |
examples |
['8080'] |
Example values in help |
deprecated |
'Use --debug' |
Deprecation warning |
hidden |
true |
Hide from help |
group |
'Advanced' |
Group in help output |
count |
true |
Count repeated flags into a number (-vvv → 3) |
variadic |
true |
Array option taking all following values (--tag a b c) |
fromFile |
true |
@path reads the value from a file, - from stdin (@@ escapes) |
conflicts |
'json' |
Options that can't be used together with this one |
implies |
{ color: false } |
Values for other options when this one is used |
requires |
'password' |
Options that must be provided along with this one |
requiredIf |
{ format: 'file' } |
Required when other options have these values |
requiredUnless |
['user', 'key'] |
Required unless one of these options is provided |
sensitive |
true |
Secret value: masked prompt, no default in help or tool schemas |
hint |
'dir' |
What shell completion offers for the value ('file', 'dir', { ext: ['json'] }, 'command', 'url', 'none') |
valueName |
'PATH' |
Value placeholder in help (--out <PATH>) |
.arguments(schema, {
positional: ['source', '...files', 'dest'],
interactive: ['name', 'template'],
optionalInteractive: ['typescript'],
fields: { verbose: { flags: 'v' } },
stdin: 'data', // or { field: 'data', trim: true }; a lone `-` reads stdin too
autoAlias: true, // default
})An optional positional before required ones (['method', 'url']) only takes a value when they still get one: http https://x sets url. Object and record options take key=value (-q page=2 -q sort=asc), JSON or dotted keys (--db.host x). Keep a meta apart from the call with defineArgsMeta(schema, meta), which types it against the schema.
Give your AI coding agent knowledge of the Padrone API:
npx skills add gkurt/padrone- Node.js 18+ or Bun
- TypeScript 5.0+ (recommended)
- Zod (or any Standard Schema-compatible library)