Prompting for Missing Values
This example demonstrates how to use cli-forge's prompt layer to interactively collect missing option values. When a prompt provider is registered, required options that were not supplied on the command line are automatically prompted for.
Prompt providers are pluggable — register one via .withPromptProvider().
This example uses a simple readline-based provider for portability.
For a richer terminal experience, cli-forge ships a ready-made
@clack/prompts provider via cli-forge/prompt-providers/clack.
Key behaviors:
required: trueoptions auto-prompt when missing (if a provider exists)prompt: trueforces prompting even when a value was already givenprompt: "label"uses a custom label instead of the descriptionprompt: (args) => ...enables conditional/dynamic prompting
1 FILESTry in Playground
All Example Files
FILE EXPLORER
prompting.ts
1import * as readline from 'node:readline';
2import cliForge, { type PromptOption, type PromptProvider } from 'cli-forge';
3
4// --- Simple readline-based prompt provider ---
5// Reads answers line-by-line from stdin. Works for both interactive
6// terminals and piped input (e.g. `printf "answer\n" | my-cli`).
7//
8// Note: When stdin is piped, readline eagerly consumes all lines.
9// Successive `rl.question()` calls race with that consumption, so we
10// buffer lines upfront when stdin is non-interactive and serve them
11// to each prompt in order.
12
13/**
14 * Collect all lines from a readable stream, then return them.
15 */
16function readAllLines(stream: NodeJS.ReadableStream): Promise<string[]> {
17 return new Promise((resolve) => {
18 const lines: string[] = [];
19 const rl = readline.createInterface({ input: stream });
20 rl.on('line', (line) => lines.push(line));
21 rl.on('close', () => resolve(lines));
22 });
23}
24
25function createReadlinePromptProvider(): PromptProvider {
26 return {
27 async promptBatch(
28 options: PromptOption[]
29 ): Promise<Record<string, unknown>> {
30 const isInteractive = !!(process.stdin as any).isTTY;
31
32 // When piped, buffer all lines first to avoid the readline race.
33 const bufferedLines = isInteractive
34 ? null
35 : await readAllLines(process.stdin);
36 let lineIndex = 0;
37
38 // For interactive mode, use question(); for piped mode, serve from buffer.
39 let rl: readline.Interface | undefined;
40 const ask = (query: string): Promise<string> => {
41 if (bufferedLines) {
42 process.stdout.write(query);
43 return Promise.resolve(bufferedLines[lineIndex++] ?? '');
44 }
45 if (rl === undefined) {
46 rl = readline.createInterface({
47 input: process.stdin,
48 output: process.stdout,
49 });
50 }
51 return new Promise((resolve) => rl!.question(query, resolve));
52 };
53
54 const results: Record<string, unknown> = {};
55 try {
56 for (const option of options) {
57 // Use custom prompt label, description, or fall back to the option name
58 const label =
59 typeof option.config.prompt === 'string'
60 ? option.config.prompt
61 : option.config.description ?? option.name;
62
63 const answer = await ask(`${label}: `);
64
65 // Coerce the raw string to the expected option type
66 if (option.config.type === 'number') {
67 results[option.name] = Number(answer);
68 } else if (option.config.type === 'boolean') {
69 results[option.name] =
70 answer.toLowerCase() === 'true' || answer === '1';
71 } else {
72 results[option.name] = answer;
73 }
74 }
75 } finally {
76 rl?.close();
77 }
78
79 return results;
80 },
81 };
82}
83
84// --- CLI definition ---
85
86const cli = cliForge('prompting-demo')
87 // Register the prompt provider. When required options are missing,
88 // cli-forge will delegate to this provider to collect values.
89 .withPromptProvider(createReadlinePromptProvider())
90 .command('$0', {
91 builder: (args) =>
92 args
93 .option('name', {
94 type: 'string',
95 description: 'Your name',
96 // Required options without a default are auto-prompted when
97 // a provider is registered and no value was given on the CLI.
98 required: true,
99 })
100 .option('greeting', {
101 type: 'string',
102 description: 'The greeting to use',
103 // Optional with a default — never prompted automatically.
104 default: 'Hello',
105 })
106 .option('age', {
107 type: 'number',
108 description: 'Your age',
109 required: true,
110 }),
111 handler: (args) => {
112 console.log(
113 `${args.greeting}, ${args.name}! You are ${args.age} years old.`
114 );
115 },
116 });
117
118export default cli;
119
120if (require.main === module) {
121 (async () => {
122 await cli.forge();
123 })();
124}
125