Interactive Prompting
Padrone can automatically prompt users for missing argument values when running in an interactive terminal. Prompt types are auto-detected from your Zod schema — booleans become confirm prompts, enums become select menus, and everything else becomes text input.
Enabling Interactivity
Section titled “Enabling Interactivity”Interactive prompting requires two things:
- Runtime support — set
interactive: trueon the runtime - Field configuration — declare which fields to prompt via
interactiveoroptionalInteractivein the arguments meta
import { createPadrone } from 'padrone';import * as z from 'zod/v4';
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'), eslint: z.boolean().default(false).describe('Add ESLint'), }), { positional: ['name'], interactive: ['name', 'template'], optionalInteractive: ['typescript', 'eslint'], } ) .action((args) => { console.log(`Creating ${args.name} with ${args.template}`); }) );
await program.cli();How It Works
Section titled “How It Works”Interactive prompting is a data acquisition step in the argument resolution pipeline. It runs after CLI args, environment variables, and config file values have been merged, but before schema validation:
CLI args → aliases → env vars → config file → interactive prompts → schema validation → actionThis means:
- Fields already provided via CLI, env, or config are never prompted
- Prompted values go through the same Zod validation as any other input
- Default values from the schema apply if a field isn’t prompted and wasn’t provided
interactive — Required Fields
Section titled “interactive — Required Fields”The interactive option controls which fields are prompted when their values are missing.
Prompt specific fields
Section titled “Prompt specific fields”{ interactive: ['name', 'template'] }Only name and template will be prompted if missing. Other missing fields rely on defaults or validation.
Prompt all required fields
Section titled “Prompt all required fields”{ interactive: true }When set to true, all fields listed in the schema’s required array that are missing will be prompted. Fields with defaults or .optional() are not prompted.
optionalInteractive — Optional Fields
Section titled “optionalInteractive — Optional Fields”After required interactive prompts are complete, optionalInteractive fields are offered in a multi-select prompt: “Would you also like to configure:” — users choose which ones to fill in.
Offer specific fields
Section titled “Offer specific fields”{ optionalInteractive: ['typescript', 'eslint', 'prettier'] }Offer all optional fields
Section titled “Offer all optional fields”{ optionalInteractive: true }When set to true, all optional fields (not in required) that are still missing will be offered.
Combined example
Section titled “Combined example”.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'), eslint: z.boolean().default(false).describe('Add ESLint'), prettier: z.boolean().default(false).describe('Add Prettier'), }), { interactive: ['name', 'template'], optionalInteractive: ['typescript', 'eslint', 'prettier'], })Running with no arguments:
- Prompts for
name(text input) - Prompts for
template(select: react / vue / svelte) - Shows multi-select: “Would you also like to configure: Use TypeScript, Add ESLint, Add Prettier”
- Prompts individually for each selected field (confirm prompts for booleans)
Running with --name myproject --template react:
- Skips all prompts — both required interactive fields are already provided
- Optional fields still offered if missing
Prompt Type Auto-Detection
Section titled “Prompt Type Auto-Detection”Padrone detects the appropriate prompt type from each field’s JSON schema:
| Schema | Prompt Type | Example |
|---|---|---|
z.boolean() | Confirm (yes/no) | Use TypeScript? (y/N) |
z.enum(['a', 'b', 'c']) | Select (single choice) | ❯ react / vue / svelte |
z.array(z.enum([...])) | Multi-select | ◯ tag1 / ◯ tag2 / ◯ tag3 |
z.string() | Text input | Project name: _ |
| Any other type | Text input | Value: _ |
Prompt messages
Section titled “Prompt messages”The prompt message is derived from (in order of priority):
fieldsmetadescription(from the arguments meta).describe()on the Zod schema property- The field name as fallback
.arguments( z.object({ name: z.string().describe('Schema description'), }), { interactive: ['name'], fields: { name: { description: 'Meta description' }, // This wins }, })Non-Interactive Runtimes
Section titled “Non-Interactive Runtimes”When runtime.interactive is false (the default) or prompt is not available, interactive prompting is silently skipped. Missing required fields will cause validation errors as usual.
This makes it safe to declare interactive in your arguments meta without breaking non-interactive environments like CI/CD pipelines, test runners, or web-based runtimes.
# Interactive terminal — prompts for missing fieldsapp init
# CI pipeline — skips prompts, validation fails if fields missingCI=true app init
# Provide all fields explicitly — works everywhereapp init myproject --template reactCustom Prompt Implementations
Section titled “Custom Prompt Implementations”The default prompt implementation uses Enquirer for terminal prompts. For non-terminal runtimes (web UIs, chat interfaces, testing), provide a custom prompt function:
program.runtime({ interactive: true, prompt: async (config) => { // config.name — field name // config.message — human-readable prompt text // config.type — 'input' | 'confirm' | 'select' | 'multiselect' | 'password' // config.choices — available choices for select/multiselect // config.default — default value from schema
// Return the user's response return await myCustomPromptUI(config); },});Testing with mock prompts
Section titled “Testing with mock prompts”import { createPadrone } from 'padrone';
const mockPrompt = async (config) => { const responses = { name: 'test-project', template: 'react' }; return responses[config.name];};
const program = createPadrone('app') .runtime({ interactive: true, prompt: mockPrompt }) .command('init', (c) => c .arguments(schema, { interactive: true }) .action((args) => args) );
const result = await program.eval('init');// result.args === { name: 'test-project', template: 'react', ... }Async Implications
Section titled “Async Implications”When interactive or optionalInteractive is set in the arguments meta, the command is automatically marked as async. This means:
cli()returnsPromise<PadroneCommandResult>parse()returnsPromise<PadroneParseResult>- You must
awaitthe result
This applies at the type level regardless of whether the runtime actually supports interactivity. TypeScript will enforce await even when the runtime is non-interactive, which is the safe default.
// With interactive meta — must awaitconst result = await program.eval('init');
// Without interactive meta — synchronousconst result = program.eval('build --target prod');parse() and run() Behavior
Section titled “parse() and run() Behavior”Interactive prompting occurs in cli() and eval(). The other execution methods behave as follows:
eval()— Parses, validates, and executes with soft error handling. Supports interactive prompting (controllable viapreferences.interactive).parse()— Parses and validates without prompting. Missing fields cause validation issues.run()— Executes with provided arguments directly. No parsing, no prompting.api()— Same asrun()— direct programmatic execution.
This keeps parse() side-effect-free and run() / api() deterministic for programmatic use.