Padrone CLI Framework
Padrone is a type-safe CLI framework for Node.js/Bun. It uses any schema library that implements the Standard Schema spec (Zod, Valibot, ArkType, etc.) for argument validation and provides an immutable builder API for defining programs, commands, interceptors, and extensions.
Installation
npm install padrone zod # or any Standard Schema-compatible library instead of zod
Quick Start
import { createPadrone } from 'padrone';
import * as z from 'zod/v4';
const program = createPadrone('mycli')
.configure({ version: '1.0.0', description: 'My CLI app' })
.command('greet', (c) =>
c
.arguments(z.object({ name: z.string() }), { positional: ['name'] })
.action((args) => `Hello, ${args.name}!`),
)
.command(['deploy', 'dp'], (c) =>
c
.arguments(z.object({
env: z.enum(['staging', 'production']),
dry: z.boolean().default(false),
}))
.action((args, { runtime }) => {
runtime.output(`Deploying to ${args.env}...`);
return { deployed: true };
}),
);
program.cli();
Core Concepts
- Immutable builder: Every method returns a new builder/program instance
- Standard Schema validation: Any schema library supporting
standard-schema (Zod, Valibot, ArkType, etc.) defines positional args, named flags, defaults, coercion, and validation
- Two entry points:
'padrone' (core) and 'padrone/test' (testing utilities)
- Sync by default: Returns become async only when async schemas or interceptors are used
Builder API Summary
| Method |
Purpose |
.arguments(schema, meta?) |
Define options/args with a Standard Schema |
.action(handler?) |
Set the command handler (args, ctx, base?) => result |
.command(name, builderFn?) |
Add or extend a subcommand |
.context(transform?) |
Define typed context or transform inherited context |
.mount(name, program, options?) |
Mount another Padrone program as a subcommand (with optional { context }) |
.configure(config) |
Set title, description, version, deprecated, hidden, group, mutation |
.intercept(interceptor) |
Register a middleware interceptor |
.extend(extension) |
Apply a build-time extension (bundle of config, commands, interceptors) |
.extend(padroneEnv(schema)) |
Parse environment variables into args (import padroneEnv from 'padrone') |
.extend(padroneConfig({ files, schema? })) |
Load args from config files (import padroneConfig from 'padrone') |
.wrap(config) |
Wrap an external CLI tool (experimental) |
.extend(padroneProgress(config?)) |
Auto-managed progress indicator (import padroneProgress from 'padrone') |
.runtime(runtime) |
Custom I/O adapter (output, error, env, prompt) |
.updateCheck(config?) |
Enable background update notifications |
.async() |
Mark command as using async validation |
Program API Summary (after builder methods)
| Method |
Purpose |
.cli(prefs?) |
Entry point from process.argv — throws on validation errors. Pass context in prefs. |
.eval(input, prefs?) |
Parse + validate + execute a string — returns issues softly. Pass context in prefs. |
.run(name, args, prefs?) |
Execute by name with args object (sync, no validation). Pass context in prefs. |
.parse(input?) |
Parse without executing |
.repl(options?) |
Start interactive REPL session |
.help(command?, prefs?) |
Generate help text |
.completion(shell?) |
Generate shell completion script |
.find(command) |
Look up a command by path |
.api() |
Type-safe programmatic API |
.tool() |
Vercel AI SDK tool definition |
.mcp(prefs?) |
Start MCP server (HTTP or stdio) (experimental) |
.serve(prefs?) |
Start REST server with OpenAPI docs (experimental) |
.stringify(command?, args?) |
Convert back to CLI string |
Arguments Meta
The second parameter to .arguments() configures positional args, interactive prompts, and field metadata:
.arguments(schema, {
positional: ['source', '...files'], // '...' prefix = variadic
interactive: true, // or ['fieldName'] for specific fields
autoAlias: true, // auto kebab-case aliases for camelCase (default: true)
stdin: 'data', // infers text/lines from schema type; use zodAsyncStream() for streaming
fields: {
output: { flags: 'o', description: 'Output path', examples: ['./dist'] },
verbose: { flags: 'v', hidden: true },
dryRun: { alias: 'dry' }, // multi-char long alias (--dry)
local: { negative: 'remote' }, // --remote sets local to false, disables --no-local
old: { deprecated: 'Use --new instead', group: 'Legacy' },
},
})
Interceptor System
Seven phases in onion/middleware pattern with next():
- start — before pipeline (root only, not called by
parse()/run())
- parse — command routing (root only)
- route — after command resolved, before validation (root + command chain)
- validate — schema validation (root + command chain)
- execute — handler execution (root + command chain)
- error — error handling, two layers: command-level first, then root-level (return
{ error: undefined, result } to suppress)
- shutdown — cleanup, always runs, two layers: command-level first, then root-level
All phase contexts include context (user-provided context), signal (AbortSignal for cancellation), caller (invocation method: 'cli', 'eval', 'run', etc.), and runtime.
import { defineInterceptor } from 'padrone';
const timer = defineInterceptor({ name: 'timer', order: -10 }, () => {
let startTime: number;
return {
start: (ctx, next) => {
startTime = Date.now();
return next();
},
execute: (ctx, next) => {
const result = next();
console.log(`${ctx.command.path} took ${Date.now() - startTime}ms`);
return result;
},
};
});
program.intercept(timer);
defineInterceptor() returns a factory — each execution gets fresh closure state. Supports .provides<T>() and .requires<T>() for typed context (type-level only).
Extension-First Architecture
Padrone's core is minimal — most features are implemented as extensions composed via .extend(). When you call createPadrone(), built-in extensions are automatically applied:
| Extension |
Order |
What it does |
signal |
-2000 |
SIGINT/SIGTERM handling, AbortSignal propagation |
autoOutput |
-1100 |
Auto-print results (strings, promises, iterators) |
color |
-1001 |
--color/--no-color flag support |
stdin |
-1001 |
Pipe stdin into argument fields |
help |
-1000 |
--help flag, help command, error-phase help display |
version |
-1000 |
--version flag |
repl |
-1000 |
--repl flag, repl command |
interactive |
-999 |
--interactive flag, auto-prompting |
suggestions |
-500 |
"Did you mean?" for unknown commands/options |
Each can be disabled: createPadrone('myapp', { builtins: { help: false } }).
Advanced opt-in extensions imported from 'padrone': padroneLogger(), padroneTiming(), padroneProgress(), padroneUpdateCheck(), padroneEnv(), padroneConfig(). Optional integrations live behind subpath imports to keep their dependencies out of the main bundle: padroneInk from 'padrone/ink', padroneMcp from 'padrone/mcp', padroneServe from 'padrone/serve', padroneTracing from 'padrone/tracing', padroneCompletion from 'padrone/completion', padroneMan from 'padrone/man'.
Testing
import { testCli } from 'padrone/test';
const result = await testCli(program).run('greet World');
// result: { command, args, result, issues, stdout, stderr, error }
// With mocks
await testCli(program)
.env({ API_KEY: 'xxx' })
.prompt({ name: 'myapp' })
.run('deploy --env staging');
// REPL testing
const { results } = await testCli(program).repl(['greet Alice', 'greet Bob']);
Progress Indicators
Auto-managed spinners for long-running commands via padroneProgress() context-providing interceptor:
.command('deploy', (c) =>
c
.async()
.extend(padroneProgress({
message: {
progress: 'Deploying...',
success: (result) => `Deployed v${result.version}`,
error: 'Deploy failed',
},
bar: true,
time: true,
eta: true,
}))
.action(async (_args, ctx) => {
await deploy();
ctx.context.progress.update(0.5);
ctx.context.progress.update('Finalizing...');
return { version: '2.0' };
}),
)
- Auto-managed:
padroneProgress() starts before execution, calls succeed/fail automatically
- Messages:
message accepts a string (progress message) or { validation?, progress?, success?, error? }. Can also be provided from context via progressConfig.message — command-level fields take precedence
- Manual control: Use
ctx.context.progress in action handlers — update(string | number | { message?, progress?, indeterminate?, time? }), succeed, fail, stop, pause, resume
- Typed context:
padroneProgress() uses .provides<{ progress: PadroneProgress }>() — ctx.context.progress is fully typed
- Dynamic messages:
success/error can be callbacks returning string | null | { message, indicator }
- Spinner config:
spinner accepts preset name ('dots', 'line', etc.), true (always show), false (disable), or { frames, interval, show } object
- Progress bar:
bar: true or bar: { width, filled, empty, animation, show } — renders percentage + bar. Indeterminate animations: 'bounce', 'slide', 'pulse'
- Elapsed time:
time: true shows ⏱ M:SS counter. Can be toggled via update({ time: true/false })
- ETA:
eta: true shows ETA M:SS based on progress rate. Requires numeric update() calls. Counts down between updates
- Custom renderer:
renderer: (message, options?) => PadroneProgress to replace the built-in terminal renderer
Error Classes
PadroneError — base (exitCode, suggestions, command, phase)
RoutingError — unknown command
ValidationError — schema failures (has .issues)
ConfigError — config file problems
ActionError — throw from action handlers with structured metadata
Additional Resources
- For the complete API reference with all type signatures, see api-reference.md
- For full working examples covering common patterns, see examples.md
1---2name: padrone-23description: Build CLI applications with the Padrone framework. Use when writing code that imports from 'padrone', creating CLI tools, defining commands with Zod schemas, or working with Padrone's builder API, interceptors, extensions, testing, REPL, or AI tool integration.4license: MIT5---67# Padrone CLI Framework89Padrone is a type-safe CLI framework for Node.js/Bun. It uses any schema library that implements the [Standard Schema](https://github.com/standard-schema/standard-schema) spec (Zod, Valibot, ArkType, etc.) for argument validation and provides an immutable builder API for defining programs, commands, interceptors, and extensions.1011## Installation1213```bash14npm install padrone zod # or any Standard Schema-compatible library instead of zod15```1617## Quick Start1819```ts20import { createPadrone } from 'padrone';21import * as z from 'zod/v4';2223const program = createPadrone('mycli')24 .configure({ version: '1.0.0', description: 'My CLI app' })25 .command('greet', (c) =>26 c27 .arguments(z.object({ name: z.string() }), { positional: ['name'] })28 .action((args) => `Hello, ${args.name}!`),29 )30 .command(['deploy', 'dp'], (c) =>31 c32 .arguments(z.object({33 env: z.enum(['staging', 'production']),34 dry: z.boolean().default(false),35 }))36 .action((args, { runtime }) => {37 runtime.output(`Deploying to ${args.env}...`);38 return { deployed: true };39 }),40 );4142program.cli();43```4445## Core Concepts4647- **Immutable builder**: Every method returns a new builder/program instance48- **Standard Schema validation**: Any schema library supporting `standard-schema` (Zod, Valibot, ArkType, etc.) defines positional args, named flags, defaults, coercion, and validation49- **Two entry points**: `'padrone'` (core) and `'padrone/test'` (testing utilities)50- **Sync by default**: Returns become async only when async schemas or interceptors are used5152## Builder API Summary5354| Method | Purpose |55|---|---|56| `.arguments(schema, meta?)` | Define options/args with a Standard Schema |57| `.action(handler?)` | Set the command handler `(args, ctx, base?) => result` |58| `.command(name, builderFn?)` | Add or extend a subcommand |59| `.context(transform?)` | Define typed context or transform inherited context |60| `.mount(name, program, options?)` | Mount another Padrone program as a subcommand (with optional `{ context }`) |61| `.configure(config)` | Set title, description, version, deprecated, hidden, group, mutation |62| `.intercept(interceptor)` | Register a middleware interceptor |63| `.extend(extension)` | Apply a build-time extension (bundle of config, commands, interceptors) |64| `.extend(padroneEnv(schema))` | Parse environment variables into args (import `padroneEnv` from `'padrone'`) |65| `.extend(padroneConfig({ files, schema? }))` | Load args from config files (import `padroneConfig` from `'padrone'`) |66| `.wrap(config)` | Wrap an external CLI tool *(experimental)* |67| `.extend(padroneProgress(config?))` | Auto-managed progress indicator (import `padroneProgress` from `'padrone'`) |68| `.runtime(runtime)` | Custom I/O adapter (output, error, env, prompt) |69| `.updateCheck(config?)` | Enable background update notifications |70| `.async()` | Mark command as using async validation |7172## Program API Summary (after builder methods)7374| Method | Purpose |75|---|---|76| `.cli(prefs?)` | Entry point from `process.argv` — throws on validation errors. Pass `context` in prefs. |77| `.eval(input, prefs?)` | Parse + validate + execute a string — returns issues softly. Pass `context` in prefs. |78| `.run(name, args, prefs?)` | Execute by name with args object (sync, no validation). Pass `context` in prefs. |79| `.parse(input?)` | Parse without executing |80| `.repl(options?)` | Start interactive REPL session |81| `.help(command?, prefs?)` | Generate help text |82| `.completion(shell?)` | Generate shell completion script |83| `.find(command)` | Look up a command by path |84| `.api()` | Type-safe programmatic API |85| `.tool()` | Vercel AI SDK tool definition |86| `.mcp(prefs?)` | Start MCP server (HTTP or stdio) *(experimental)* |87| `.serve(prefs?)` | Start REST server with OpenAPI docs *(experimental)* |88| `.stringify(command?, args?)` | Convert back to CLI string |8990## Arguments Meta9192The second parameter to `.arguments()` configures positional args, interactive prompts, and field metadata:9394```ts95.arguments(schema, {96 positional: ['source', '...files'], // '...' prefix = variadic97 interactive: true, // or ['fieldName'] for specific fields98 autoAlias: true, // auto kebab-case aliases for camelCase (default: true)99 stdin: 'data', // infers text/lines from schema type; use zodAsyncStream() for streaming100 fields: {101 output: { flags: 'o', description: 'Output path', examples: ['./dist'] },102 verbose: { flags: 'v', hidden: true },103 dryRun: { alias: 'dry' }, // multi-char long alias (--dry)104 local: { negative: 'remote' }, // --remote sets local to false, disables --no-local105 old: { deprecated: 'Use --new instead', group: 'Legacy' },106 },107})108```109110## Interceptor System111112Seven phases in onion/middleware pattern with `next()`:1131141. **start** — before pipeline (root only, not called by `parse()`/`run()`)1152. **parse** — command routing (root only)1163. **route** — after command resolved, before validation (root + command chain)1174. **validate** — schema validation (root + command chain)1185. **execute** — handler execution (root + command chain)1196. **error** — error handling, two layers: command-level first, then root-level (return `{ error: undefined, result }` to suppress)1207. **shutdown** — cleanup, always runs, two layers: command-level first, then root-level121122All phase contexts include `context` (user-provided context), `signal` (AbortSignal for cancellation), `caller` (invocation method: `'cli'`, `'eval'`, `'run'`, etc.), and `runtime`.123124```ts125import { defineInterceptor } from 'padrone';126127const timer = defineInterceptor({ name: 'timer', order: -10 }, () => {128 let startTime: number;129 return {130 start: (ctx, next) => {131 startTime = Date.now();132 return next();133 },134 execute: (ctx, next) => {135 const result = next();136 console.log(`${ctx.command.path} took ${Date.now() - startTime}ms`);137 return result;138 },139 };140});141program.intercept(timer);142```143144`defineInterceptor()` returns a factory — each execution gets fresh closure state. Supports `.provides<T>()` and `.requires<T>()` for typed context (type-level only).145146## Extension-First Architecture147148Padrone's core is minimal — most features are implemented as extensions composed via `.extend()`. When you call `createPadrone()`, built-in extensions are automatically applied:149150| Extension | Order | What it does |151|-----------|-------|-------------|152| `signal` | -2000 | SIGINT/SIGTERM handling, AbortSignal propagation |153| `autoOutput` | -1100 | Auto-print results (strings, promises, iterators) |154| `color` | -1001 | `--color`/`--no-color` flag support |155| `stdin` | -1001 | Pipe stdin into argument fields |156| `help` | -1000 | `--help` flag, `help` command, error-phase help display |157| `version` | -1000 | `--version` flag |158| `repl` | -1000 | `--repl` flag, `repl` command |159| `interactive` | -999 | `--interactive` flag, auto-prompting |160| `suggestions` | -500 | "Did you mean?" for unknown commands/options |161162Each can be disabled: `createPadrone('myapp', { builtins: { help: false } })`.163164Advanced opt-in extensions imported from `'padrone'`: `padroneLogger()`, `padroneTiming()`, `padroneProgress()`, `padroneUpdateCheck()`, `padroneEnv()`, `padroneConfig()`. Optional integrations live behind subpath imports to keep their dependencies out of the main bundle: `padroneInk` from `'padrone/ink'`, `padroneMcp` from `'padrone/mcp'`, `padroneServe` from `'padrone/serve'`, `padroneTracing` from `'padrone/tracing'`, `padroneCompletion` from `'padrone/completion'`, `padroneMan` from `'padrone/man'`.165166## Testing167168```ts169import { testCli } from 'padrone/test';170171const result = await testCli(program).run('greet World');172// result: { command, args, result, issues, stdout, stderr, error }173174// With mocks175await testCli(program)176 .env({ API_KEY: 'xxx' })177 .prompt({ name: 'myapp' })178 .run('deploy --env staging');179180// REPL testing181const { results } = await testCli(program).repl(['greet Alice', 'greet Bob']);182```183184## Progress Indicators185186Auto-managed spinners for long-running commands via `padroneProgress()` context-providing interceptor:187188```ts189.command('deploy', (c) =>190 c191 .async()192 .extend(padroneProgress({193 message: {194 progress: 'Deploying...',195 success: (result) => `Deployed v${result.version}`,196 error: 'Deploy failed',197 },198 bar: true,199 time: true,200 eta: true,201 }))202 .action(async (_args, ctx) => {203 await deploy();204 ctx.context.progress.update(0.5);205 ctx.context.progress.update('Finalizing...');206 return { version: '2.0' };207 }),208)209```210211- **Auto-managed**: `padroneProgress()` starts before execution, calls `succeed`/`fail` automatically212- **Messages**: `message` accepts a string (progress message) or `{ validation?, progress?, success?, error? }`. Can also be provided from context via `progressConfig.message` — command-level fields take precedence213- **Manual control**: Use `ctx.context.progress` in action handlers — `update(string | number | { message?, progress?, indeterminate?, time? })`, `succeed`, `fail`, `stop`, `pause`, `resume`214- **Typed context**: `padroneProgress()` uses `.provides<{ progress: PadroneProgress }>()` — `ctx.context.progress` is fully typed215- **Dynamic messages**: `success`/`error` can be callbacks returning `string | null | { message, indicator }`216- **Spinner config**: `spinner` accepts preset name (`'dots'`, `'line'`, etc.), `true` (always show), `false` (disable), or `{ frames, interval, show }` object217- **Progress bar**: `bar: true` or `bar: { width, filled, empty, animation, show }` — renders percentage + bar. Indeterminate animations: `'bounce'`, `'slide'`, `'pulse'`218- **Elapsed time**: `time: true` shows `⏱ M:SS` counter. Can be toggled via `update({ time: true/false })`219- **ETA**: `eta: true` shows `ETA M:SS` based on progress rate. Requires numeric `update()` calls. Counts down between updates220- **Custom renderer**: `renderer: (message, options?) => PadroneProgress` to replace the built-in terminal renderer221222## Error Classes223224- `PadroneError` — base (exitCode, suggestions, command, phase)225- `RoutingError` — unknown command226- `ValidationError` — schema failures (has `.issues`)227- `ConfigError` — config file problems228- `ActionError` — throw from action handlers with structured metadata229230## Additional Resources231232- For the complete API reference with all type signatures, see [api-reference.md](api-reference.md)233- For full working examples covering common patterns, see [examples.md](examples.md)