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: true options auto-prompt when missing (if a provider exists)
  • prompt: true forces prompting even when a value was already given
  • prompt: "label" uses a custom label instead of the description
  • prompt: (args) => ... enables conditional/dynamic prompting

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