Skip to content

Commands & Arguments

This guide covers how to work with commands, arguments, positional arguments, and nested command hierarchies in Padrone.

Arguments are defined using Zod schemas. Each property in the schema becomes a CLI argument:

import { createPadrone } from 'padrone';
import * as z from 'zod/v4';
const program = createPadrone('app')
.arguments(
z.object({
port: z.number().default(3000).describe('Port to listen on'),
host: z.string().default('localhost').describe('Host to bind to'),
verbose: z.boolean().optional().describe('Enable verbose logging'),
})
)
.action((args, ctx) => {
// args: { port: number; host: string; verbose?: boolean }
// ctx: { runtime, command, program, progress, context }
});

Padrone supports these Zod types:

Zod TypeCLI Input Example
z.string()--name "John"
z.number()--port 3000
z.boolean()--verbose or --no-verbose (customizable via negative meta)
z.enum(['a', 'b'])--level high
z.array(z.string())--tags foo --tags bar or --tags=[foo,bar]

Add short flags using .meta():

z.object({
port: z.number().default(3000).meta({ flags: 'p' }),
verbose: z.boolean().optional().meta({ flags: 'v' }),
})

Users can now use -p 8080 instead of --port 8080. Short flags are single-character and stackable: -vp 8080 = -v -p 8080.

The .meta() method supports several properties:

z.string().meta({
flags: 'o', // Short flag (-o)
alias: 'out', // Long alias (--out)
negative: 'remote', // Custom negation keyword (booleans only)
examples: ['file.txt'], // Example values for help text
deprecated: 'Use --out', // Deprecation warning
hidden: true, // Hide from help output
group: 'Output', // Group in help output
})

Note: Single-character short flags use flags, not alias. The alias field is for multi-character long alternatives. By default, camelCase names automatically get kebab-case aliases (e.g., dryRun--dry-run). For booleans, negative defines custom keyword(s) that set the option to false and disables the default --no- prefix (see Arguments Metadata reference).

Positional arguments let users provide values without argument names:

.arguments(
z.object({
source: z.string().describe('Source file'),
dest: z.string().describe('Destination file'),
}),
{ positional: ['source', 'dest'] }
)
Terminal window
# Both are equivalent:
app copy file.txt backup.txt
app copy --source file.txt --dest backup.txt

Use ... prefix for variadic (rest) arguments that capture multiple values:

.arguments(
z.object({
files: z.array(z.string()).describe('Files to process'),
output: z.string().describe('Output directory'),
}),
{ positional: ['...files', 'output'] }
)
Terminal window
app process a.txt b.txt c.txt ./out
# files: ['a.txt', 'b.txt', 'c.txt'], output: './out'

Add commands using the .command() method:

const program = createPadrone('git')
.command('clone', (c) =>
c
.arguments(
z.object({
url: z.string().describe('Repository URL'),
depth: z.number().optional().describe('Clone depth'),
}),
{ positional: ['url'] }
)
.action((args) => {
console.log(`Cloning ${args.url}`);
})
)
.command('status', (c) =>
c.action(() => {
console.log('On branch main');
})
);

Configure commands with .configure():

.command('serve', (c) =>
c
.configure({
title: 'Dev Server',
description: 'Start the development server',
})
.arguments(schema)
.action(handler)
)

Commands can contain subcommands to any depth:

const program = createPadrone('db')
.command('migrate', (c) =>
c
.command('up', (c) =>
c.action(() => console.log('Running migrations'))
)
.command('down', (c) =>
c
.arguments(z.object({ steps: z.number().default(1) }))
.action((args) => console.log(`Rolling back ${args.steps} migrations`))
)
.command('status', (c) =>
c.action(() => console.log('Migration status'))
)
);
Terminal window
db migrate up
db migrate down --steps 3
db migrate status

Bind arguments to environment variables using the padroneEnv extension:

import { createPadrone, padroneEnv } from 'padrone';
import * as z from 'zod/v4';
const program = createPadrone('app')
.command('serve', (c) =>
c
.arguments(
z.object({
port: z.number().default(3000),
apiKey: z.string().describe('API key'),
}),
)
.extend(
padroneEnv(
z.object({
APP_PORT: z.coerce.number().optional(),
API_KEY: z.string().optional(),
}).transform((env) => ({
port: env.APP_PORT,
apiKey: env.API_KEY,
}))
)
)
.action((args) => {
console.log(`Server on port ${args.port}`);
}),
);

The env schema validates process.env and transforms env var names into argument names. padroneEnv can be applied at the program level (inherited by all commands) or at the command level.

Priority order: CLI argument > Stdin > Environment variable > Config file > Interactive prompt > Default value

Load arguments from configuration files using the padroneConfig extension:

import { createPadrone, padroneConfig } from 'padrone';
import * as z from 'zod/v4';
const program = createPadrone('app')
.command('serve', (c) =>
c
.arguments(
z.object({
port: z.number().default(3000),
host: z.string().default('localhost'),
}),
)
.extend(
padroneConfig({
files: ['app.config.json', '.apprc'],
schema: z.object({
port: z.number().optional(),
host: z.string().optional(),
}),
})
)
.action((args) => {
console.log(`Server on ${args.host}:${args.port}`);
}),
);

Multiple config file paths can be provided in the files array — the first existing file is used. If no schema is provided, config values are matched against the argument schema directly. padroneConfig can be applied at the program level (inherited by all commands) or at the command level.

Priority order: CLI argument > Stdin > Environment variable > Config file > Interactive prompt > Default value

Commands can prompt users for missing field values when running in an interactive terminal. This is configured in the arguments meta and requires the runtime to have interactive: true.

const program = createPadrone('app')
.runtime({ interactive: true })
.command('init', (c) =>
c
.arguments(
z.object({
name: z.string().describe('Project name'),
template: z.enum(['react', 'vue', 'svelte']).describe('Starter template'),
typescript: z.boolean().default(false).describe('Use TypeScript'),
}),
{
interactive: ['name', 'template'],
optionalInteractive: ['typescript'],
}
)
.action((args) => {
console.log(`Creating ${args.name} with ${args.template}`);
})
);

Running app init without arguments will:

  1. Prompt for name (text input) and template (select from enum choices)
  2. Ask “Would you also like to configure:” with typescript as a choice
  3. Prompt for any selected optional fields

Values provided via CLI, env vars, or config files skip the prompt. Running app init myproject --template react only prompts for nothing — all required interactive fields are already provided.

Interactive prompting only occurs in cli() and eval(), not in parse() or run(). See the Interactive Prompting guide for full details.

Padrone automatically generates help text:

// Print help for the program
console.log(program.help());
// Print help for a specific command
console.log(program.help('migrate up'));
// Different formats
program.help('', { format: 'text' }); // Plain text
program.help('', { format: 'ansi' }); // With colors
program.help('', { format: 'markdown' });
program.help('', { format: 'html' });
program.help('', { format: 'json' });

Re-registering a command with the same name merges the new definition with the existing one. The new handler receives the previous handler as a base parameter:

const program = createPadrone('app')
.command('deploy', (c) =>
c
.arguments(z.object({ target: z.string() }))
.action((args) => `deploying to ${args.target}`)
)
.command('deploy', (c) =>
c.action((args, ctx, base) => {
console.log('Pre-deploy hook');
const result = base(args, ctx);
console.log('Post-deploy hook');
return result;
})
);

Configuration is shallow-merged, subcommands are recursively merged by name, and aliases are preserved from the original when the override doesn’t specify new ones. See the Program Composition guide for full details.

Look up commands programmatically:

const migrateUp = program.find('migrate up');
if (migrateUp) {
console.log(migrateUp.name); // 'up'
}