Coding
PromptBeginner5 minmarkdown
Markdown Converter
Agent skill for markdown-converter
21
This specification defines the **Interactive Prompts** feature for `@alcyone-labs/arg-parser`, which integrates `@clack/prompts` to provide command-level interactive mode. This allows CLI builders to offer both programmatic (flag-based) and interactive (prompt-based) interfaces for the same commands
Sign in to like and favorite skills
This specification defines the Interactive Prompts feature for
@alcyone-labs/arg-parser, which integrates @clack/prompts to provide command-level interactive mode. This allows CLI builders to offer both programmatic (flag-based) and interactive (prompt-based) interfaces for the same commands.
--env production) and interactive prompts| Term | Definition |
|---|---|
| Interactive Mode | State where prompts are shown to collect missing values |
| Promptable Flag | A flag that has a configuration for interactive mode |
| Prompt Sequence | The order in which prompts are displayed |
| Prompt When | Condition that triggers interactive mode for a command |
| Prompt Answer | Value collected from a prompt, stored in |
Interactivity is configured at the command level via
promptWhen:
interface IInteractiveSubCommand extends ISubCommand { /** When to trigger interactive prompts for this command */ promptWhen?: 'interactive-flag' | 'missing' | 'always'; /** Called when user cancels (Ctrl+C) during prompts */ onCancel?: (ctx: IHandlerContext) => void | Promise<void>; }
Modes:
'interactive-flag' (default): Prompts shown only when --interactive or -i flag is present'missing': Prompts shown when any promptable flag is missing a value'always': Always show prompts (overrides CLI args for promptable flags)Individual flags opt-in to prompts via the
prompt property:
interface IPromptableFlag extends IFlag { /** * Prompt configuration factory. * If provided, this flag can participate in interactive mode. */ prompt?: (ctx: IHandlerContext) => PromptFieldConfig | Promise<PromptFieldConfig>; /** * Explicit sequence order (1 = first, 2 = second). * If omitted, uses the flag's position in the parser's flag array. */ promptSequence?: number; }
Key behaviors:
prompt are excluded from interactive mode entirelyprompt are included in the sequencepromptSequence is optional; array order is the fallbackinterface PromptFieldConfig { /** Type of prompt to display */ type: 'text' | 'password' | 'confirm' | 'select' | 'multiselect'; /** Message shown to the user */ message: string; /** Placeholder text (for text/password types) */ placeholder?: string; /** Initial/default value */ initial?: any; /** * Validation function. * Return true for valid, string for error message. * Can be async. */ validate?: (value: any, ctx: IHandlerContext) => boolean | string | Promise<boolean | string>; /** * Options for select/multiselect. * Can be simple strings or label/value objects. */ options?: Array<string | { label: string; value: any; hint?: string }>; /** Maximum items to show before scrolling (select/multiselect) */ maxItems?: number; }
interface IHandlerContext { // ... existing fields ... /** * Answers collected from interactive prompts. * Populated sequentially as prompts are answered. * Available to subsequent prompts for conditional logic. * Also available to the final handler. */ promptAnswers?: Record<string, any>; /** Whether running in interactive mode (prompts were shown) */ isInteractive?: boolean; }
┌─────────────────────────────────────────────────────────────┐ │ 1. Parse CLI Arguments │ │ - Parse all flags normally │ │ - Include --interactive if present │ └──────────────────────┬──────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────┐ │ 2. Check Interactive Trigger │ │ - Get command's promptWhen setting │ │ - Check if condition is met: │ │ • 'interactive-flag': --interactive present? │ │ • 'missing': Any promptable flag missing value? │ │ • 'always': Always true │ └──────────────────────┬──────────────────────────────────────┘ │ ┌───────────┴───────────┐ │ │ ┌─────▼─────┐ ┌──────▼──────┐ │ Condition │ │ Condition │ │ NOT MET │ │ MET │ └─────┬─────┘ └──────┬──────┘ │ │ ▼ ▼ ┌──────────────────┐ ┌──────────────────────────────────────┐ │ Skip Prompts │ │ 3. Collect Promptable Flags │ │ Execute Handler │ │ - Filter flags with 'prompt' │ │ (normal flow) │ │ - Sort by promptSequence │ └──────────────────┘ │ - Fallback to array order │ └──────────────┬───────────────────────┘ │ ▼ ┌──────────────────────────────────────┐ │ 4. Execute Prompts Sequentially │ │ │ │ For each flag in sequence: │ │ ┌─────────────────────────────┐ │ │ │ a. Call flag.prompt(ctx) │ │ │ │ - ctx.promptAnswers has │ │ │ │ previous answers │ │ │ └──────────────┬──────────────┘ │ │ │ │ │ ▼ │ │ ┌─────────────────────────────┐ │ │ │ b. Execute @clack/prompts │ │ │ │ - Show interactive UI │ │ │ │ - Wait for user input │ │ │ └──────────────┬──────────────┘ │ │ │ │ │ ▼ │ │ ┌─────────────────────────────┐ │ │ │ c. Validate Result │ │ │ │ - Call validate() │ │ │ │ - If invalid: re-prompt │ │ │ │ (infinite loop) │ │ │ └──────────────┬──────────────┘ │ │ │ │ │ ▼ │ │ ┌─────────────────────────────┐ │ │ │ d. Store Answer │ │ │ │ - ctx.promptAnswers[name]│ │ │ │ = user value │ │ │ └─────────────────────────────┘ │ │ │ └──────────────────┬───────────────────┘ │ ▼ ┌──────────────────────────────────────┐ │ 5. Execute Handler │ │ - ctx.promptAnswers populated │ │ - ctx.isInteractive = true │ │ - Handler processes answers │ └──────────────────────────────────────┘
onCancel handler if providedprocess.stdin.isTTY === false"Missing required value for --{flag}. Run with --interactive or provide the flag."promptWhen but no flags have promptpromptSequenceimport { ArgParser } from '@alcyone-labs/arg-parser'; const cli = new ArgParser({ appName: 'my-cli', promptWhen: 'interactive-flag' }); // Add --interactive flag cli.addFlag({ name: 'interactive', options: ['--interactive', '-i'], type: 'boolean', flagOnly: true, description: 'Run in interactive mode' }); // Add promptable flags cli.addFlag({ name: 'name', options: ['--name', '-n'], type: 'string', description: 'Your name', prompt: async () => ({ type: 'text', message: 'What is your name?', placeholder: 'John Doe', validate: (val) => val.length > 0 || 'Name is required' }) }); cli.addFlag({ name: 'project', options: ['--project', '-p'], type: 'string', description: 'Project type', prompt: async () => ({ type: 'select', message: 'Select project type:', options: [ { label: 'Web Application', value: 'web', hint: 'React/Vue/Angular' }, { label: 'API Server', value: 'api', hint: 'REST/GraphQL' }, { label: 'CLI Tool', value: 'cli', hint: 'Node.js/Bun' } ] }) }); cli.setHandler(async (ctx) => { if (ctx.isInteractive) { console.log('Interactive answers:', ctx.promptAnswers); } console.log('Hello', ctx.args.name || ctx.promptAnswers?.name); }); await cli.parse();
Usage:
# Programmatic my-cli --name "Alice" --project web # Interactive my-cli --interactive # ? What is your name? Alice # ? Select project type: # Web Application (React/Vue/Angular) # API Server (REST/GraphQL) # ❯ CLI Tool (Node.js/Bun)
const deployParser = new ArgParser({ appName: 'deploy', promptWhen: 'interactive-flag' }); deployParser.addFlag({ name: 'environment', options: ['--env', '-e'], type: 'string', promptSequence: 1, prompt: async () => ({ type: 'select', message: 'Select environment:', options: ['staging', 'production'] }) }); deployParser.addFlag({ name: 'version', options: ['--version', '-v'], type: 'string', promptSequence: 2, // Dynamic options based on selected environment prompt: async (ctx) => { const env = ctx.promptAnswers?.environment; const versions = await fetchVersions(env); // Different versions per env return { type: 'select', message: `Select version for ${env}:`, options: versions.map(v => ({ label: v.name, value: v.id, hint: v.deployedAt })) }; } }); deployParser.addFlag({ name: 'force', options: ['--force', '-f'], type: 'boolean', promptSequence: 3, prompt: async (ctx) => ({ type: 'confirm', message: `Deploy ${ctx.promptAnswers?.version} to ${ctx.promptAnswers?.environment}?`, initial: false }) });
const cli = new ArgParser({ appName: 'git-helper' }); const initParser = new ArgParser({ appName: 'init', handler: async (ctx) => { console.log('Initializing repo:', ctx.promptAnswers?.name); } }); initParser.addFlag({ name: 'name', options: ['--name', '-n'], type: 'string', mandatory: true, // Required flag prompt: async () => ({ type: 'text', message: 'Repository name:', validate: (v) => /^[a-z0-9-]+$/.test(v) || 'Use lowercase, numbers, and hyphens' }) }); initParser.addFlag({ name: 'visibility', options: ['--visibility', '-v'], type: 'string', defaultValue: 'private', prompt: async () => ({ type: 'select', message: 'Visibility:', options: [ { label: 'Public', value: 'public' }, { label: 'Private', value: 'private' } ] }) }); cli.addSubCommand({ name: 'init', description: 'Initialize a repository', promptWhen: 'missing', // Prompt if --name is missing parser: initParser, onCancel: () => console.log('Initialization cancelled') }); await cli.parse();
Usage:
# Provide all args - no prompts git-helper init --name my-repo --visibility public # Missing --name - triggers prompt git-helper init # ? Repository name: my-repo # ? Visibility: # Public # ❯ Private
@clack/prompts: ^0.x.x (regular dependency)chalk (already used by @clack/prompts)Status: Ready for implementation
Priority: High
Estimated Effort: 12-16 hours
Dependencies: @clack/prompts