0
# Interactive Terminal Questions
1
2
Ask questions in terminal with option validation, retry mechanisms, and customizable input streams for building interactive command-line applications.
3
4
## Capabilities
5
6
### Main Ask Function
7
8
Displays questions and waits for user responses with optional validation and retry logic.
9
10
```typescript { .api }
11
/**
12
* Ask an interactive question and wait for user response
13
* @param question - The question text to display
14
* @param config - Configuration options for validation and behavior
15
* @returns Promise resolving to the user's answer as string
16
*/
17
function ask(question: string, config?: AskConfig): Promise<string>;
18
19
interface AskConfig {
20
/** Array of valid answer options for validation */
21
options?: string[];
22
/** Maximum number of retry attempts on invalid input (default: 3) */
23
maxRetries?: number;
24
/** Custom input stream (defaults to process.stdin) */
25
inputStream?: any;
26
}
27
```
28
29
**Usage Examples:**
30
31
```typescript
32
import { ask } from "stdio";
33
34
// Basic question
35
const name = await ask("What's your name?");
36
console.log(`Hello, ${name}!`);
37
38
// Multiple choice with validation
39
const choice = await ask("Choose an option", {
40
options: ['yes', 'no', 'maybe']
41
});
42
console.log(`You chose: ${choice}`);
43
44
// Custom retry limit
45
const action = await ask("Confirm action", {
46
options: ['confirm', 'cancel'],
47
maxRetries: 5
48
});
49
50
// Free-form input (no validation)
51
const feedback = await ask("Any additional comments?");
52
53
// Complex interactive flow
54
const language = await ask("Select language", {
55
options: ['javascript', 'typescript', 'python']
56
});
57
58
const framework = language === 'javascript' || language === 'typescript'
59
? await ask("Select framework", { options: ['react', 'vue', 'angular'] })
60
: await ask("Select framework", { options: ['django', 'flask', 'fastapi'] });
61
62
console.log(`Selected: ${language} with ${framework}`);
63
```
64
65
### Configuration Interface
66
67
Comprehensive configuration for question behavior and validation.
68
69
```typescript { .api }
70
interface AskConfig {
71
/**
72
* Valid answer options - if provided, only these answers are accepted
73
* Invalid answers trigger retry with helpful error message
74
*/
75
options?: string[];
76
77
/**
78
* Maximum retry attempts for invalid answers (default: 3)
79
* After exhausting retries, promise rejects with error
80
*/
81
maxRetries?: number;
82
83
/**
84
* Custom input stream for testing or alternative input sources
85
* Defaults to process.stdin for normal terminal interaction
86
*/
87
inputStream?: any;
88
}
89
```
90
91
### Question Display Format
92
93
Questions are automatically formatted with helpful context:
94
95
```typescript
96
// Basic question
97
await ask("Enter your email");
98
// Displays: "Enter your email: "
99
100
// With options
101
await ask("Continue?", { options: ['yes', 'no'] });
102
// Displays: "Continue? [yes/no]: "
103
104
// Multiple options
105
await ask("Select environment", {
106
options: ['development', 'staging', 'production']
107
});
108
// Displays: "Select environment [development/staging/production]: "
109
```
110
111
## Advanced Features
112
113
### Input Validation and Retry Logic
114
115
Automatic validation with user-friendly error handling:
116
117
```typescript
118
import { ask } from "stdio";
119
120
// This will retry until valid input
121
const answer = await ask("Are you sure?", {
122
options: ['yes', 'no'],
123
maxRetries: 3
124
});
125
126
// Invalid inputs show helpful messages:
127
// User types "maybe"
128
// Output: "Unexpected answer. 2 retries left."
129
// Prompt: "Are you sure? [yes/no]: "
130
131
// After 3 invalid attempts, promise rejects with:
132
// Error: "Retries spent"
133
```
134
135
### Custom Input Streams
136
137
Support for alternative input sources for testing and automation:
138
139
```typescript
140
import { ask } from "stdio";
141
import { Readable } from 'stream';
142
143
// Create mock input stream for testing
144
const mockInput = new Readable({
145
read() {
146
this.push('yes\n');
147
this.push(null); // End stream
148
}
149
});
150
151
const response = await ask("Proceed?", {
152
options: ['yes', 'no'],
153
inputStream: mockInput
154
});
155
// response === 'yes'
156
157
// Testing interactive flows
158
async function testInteractiveFlow() {
159
const responses = ['john', 'developer', 'yes'];
160
let responseIndex = 0;
161
162
const mockStream = new Readable({
163
read() {
164
if (responseIndex < responses.length) {
165
this.push(responses[responseIndex++] + '\n');
166
} else {
167
this.push(null);
168
}
169
}
170
});
171
172
const name = await ask("Name?", { inputStream: mockStream });
173
const role = await ask("Role?", { inputStream: mockStream });
174
const confirm = await ask("Confirm?", {
175
options: ['yes', 'no'],
176
inputStream: mockStream
177
});
178
179
return { name, role, confirm };
180
}
181
```
182
183
### Error Handling
184
185
Comprehensive error handling for various failure scenarios:
186
187
```typescript
188
try {
189
const answer = await ask("Choose option", {
190
options: ['a', 'b', 'c'],
191
maxRetries: 2
192
});
193
console.log(`Selected: ${answer}`);
194
} catch (error) {
195
if (error.message === 'Retries spent') {
196
console.error('Too many invalid attempts');
197
process.exit(1);
198
} else {
199
console.error('Unexpected error:', error.message);
200
}
201
}
202
203
// Stream-related errors
204
try {
205
const brokenStream = new BrokenReadableStream();
206
await ask("Question?", { inputStream: brokenStream });
207
} catch (error) {
208
console.error('Input stream error:', error);
209
}
210
```
211
212
### Integration Patterns
213
214
Common patterns for building interactive applications:
215
216
**Setup Wizard:**
217
218
```typescript
219
async function setupWizard() {
220
console.log('=== Application Setup ===');
221
222
const config = {};
223
224
config.name = await ask("Project name?");
225
226
config.type = await ask("Project type?", {
227
options: ['web', 'api', 'cli']
228
});
229
230
if (config.type === 'web') {
231
config.framework = await ask("Frontend framework?", {
232
options: ['react', 'vue', 'angular']
233
});
234
}
235
236
config.database = await ask("Database?", {
237
options: ['mysql', 'postgresql', 'mongodb', 'none']
238
});
239
240
const proceed = await ask("Create project with these settings?", {
241
options: ['yes', 'no']
242
});
243
244
if (proceed === 'yes') {
245
await createProject(config);
246
} else {
247
console.log('Setup cancelled');
248
}
249
}
250
```
251
252
**Confirmation Dialogs:**
253
254
```typescript
255
async function confirmAction(action: string): Promise<boolean> {
256
const response = await ask(`Are you sure you want to ${action}?`, {
257
options: ['yes', 'no'],
258
maxRetries: 1
259
});
260
return response === 'yes';
261
}
262
263
// Usage
264
if (await confirmAction('delete all files')) {
265
await deleteFiles();
266
} else {
267
console.log('Operation cancelled');
268
}
269
```
270
271
**Menu Selection:**
272
273
```typescript
274
async function showMenu(): Promise<string> {
275
console.log('Available actions:');
276
console.log('1. Create new item');
277
console.log('2. List items');
278
console.log('3. Delete item');
279
console.log('4. Exit');
280
281
const choice = await ask("Select action", {
282
options: ['1', '2', '3', '4'],
283
maxRetries: 5
284
});
285
286
const actions = {
287
'1': 'create',
288
'2': 'list',
289
'3': 'delete',
290
'4': 'exit'
291
};
292
293
return actions[choice];
294
}
295
```
296
297
## Constants and Defaults
298
299
```typescript { .api }
300
/** Default maximum retry attempts */
301
const DEFAULT_MAX_RETRIES = 3;
302
```
303
304
The ask function uses sensible defaults:
305
- **Input Stream**: `process.stdin` for normal terminal interaction
306
- **Max Retries**: 3 attempts before rejecting with error
307
- **Output**: Questions display to `process.stdout` with formatted options
308
- **Validation**: Case-sensitive exact matching for option validation