padrone
v3.0.0
Published
Create type-safe, interactive CLI apps with Zod schemas
Maintainers
Readme
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.
Install
npm install padrone zodScaffold a New Project
The fastest way to get started is with padrone init:
npx padrone init my-cli
cd my-cli && bun i && bun devQuick Start
import { 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!What It Does
// 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, ansiFeatures at a Glance
Arguments — 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'] }).
API
Builder (define commands)
| 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 |
Program (run commands)
| 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 |
Zod .meta() fields
| 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 meta (second param of .arguments())
.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.
Agent Skill
Give your AI coding agent knowledge of the Padrone API:
npx skills add gkurt/padroneRequirements
- Node.js 18+ or Bun
- TypeScript 5.0+ (recommended)
- Zod (or any Standard Schema-compatible library)
