genomic 0.0.1 → 4.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -1
- package/README.md +1214 -1
- package/commander.d.ts +21 -0
- package/commander.js +57 -0
- package/esm/commander.js +50 -0
- package/esm/index.js +4 -0
- package/esm/keypress.js +95 -0
- package/esm/prompt.js +1024 -0
- package/esm/question/index.js +1 -0
- package/esm/question/types.js +1 -0
- package/esm/resolvers/date.js +11 -0
- package/esm/resolvers/git.js +26 -0
- package/esm/resolvers/index.js +103 -0
- package/esm/resolvers/npm.js +24 -0
- package/esm/resolvers/types.js +1 -0
- package/esm/resolvers/workspace.js +141 -0
- package/esm/utils.js +12 -0
- package/index.d.ts +4 -0
- package/index.js +20 -0
- package/keypress.d.ts +45 -0
- package/keypress.js +99 -0
- package/package.json +32 -64
- package/prompt.d.ts +116 -0
- package/prompt.js +1032 -0
- package/question/index.d.ts +1 -0
- package/question/index.js +17 -0
- package/question/types.d.ts +65 -0
- package/question/types.js +2 -0
- package/resolvers/date.d.ts +5 -0
- package/resolvers/date.js +14 -0
- package/resolvers/git.d.ts +11 -0
- package/resolvers/git.js +30 -0
- package/resolvers/index.d.ts +63 -0
- package/resolvers/index.js +111 -0
- package/resolvers/npm.d.ts +10 -0
- package/resolvers/npm.js +28 -0
- package/resolvers/types.d.ts +12 -0
- package/resolvers/types.js +2 -0
- package/resolvers/workspace.d.ts +6 -0
- package/resolvers/workspace.js +144 -0
- package/utils.d.ts +2 -0
- package/utils.js +16 -0
- package/main/index.js +0 -7
package/README.md
CHANGED
|
@@ -1 +1,1214 @@
|
|
|
1
|
-
#
|
|
1
|
+
# genomic
|
|
2
|
+
|
|
3
|
+
<p align="center" width="100%">
|
|
4
|
+
<img height="90" src="https://raw.githubusercontent.com/constructive-io/dev-utils/refs/heads/main/docs/img/genomic.svg" />
|
|
5
|
+
</p>
|
|
6
|
+
|
|
7
|
+
<p align="center" width="100%">
|
|
8
|
+
<a href="https://github.com/constructive-io/dev-utils/actions/workflows/ci.yml">
|
|
9
|
+
<img height="20" src="https://github.com/constructive-io/dev-utils/actions/workflows/ci.yml/badge.svg" />
|
|
10
|
+
</a>
|
|
11
|
+
<a href="https://github.com/constructive-io/dev-utils/blob/main/LICENSE">
|
|
12
|
+
<img height="20" src="https://img.shields.io/badge/license-MIT-blue.svg"/>
|
|
13
|
+
</a>
|
|
14
|
+
<a href="https://www.npmjs.com/package/genomic"><img height="20" src="https://img.shields.io/npm/dt/genomic"></a>
|
|
15
|
+
<a href="https://www.npmjs.com/package/genomic"><img height="20" src="https://img.shields.io/github/package-json/v/constructive-io/dev-utils?filename=packages%2Fgenomic%2Fpackage.json"></a>
|
|
16
|
+
</p>
|
|
17
|
+
|
|
18
|
+
> Formerly [`inquirerer`](https://www.npmjs.com/package/inquirerer)
|
|
19
|
+
|
|
20
|
+
A powerful, TypeScript-first library for building beautiful command-line interfaces.Create interactive CLI tools with ease using intuitive prompts, validation, and rich user experiences.
|
|
21
|
+
|
|
22
|
+
## Installation
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
npm install genomic
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Features
|
|
29
|
+
|
|
30
|
+
- 🔌 **CLI Builder** - Build command-line utilties fast
|
|
31
|
+
- 🖊 **Multiple Question Types** - Support for text, autocomplete, checkbox, and confirm questions
|
|
32
|
+
- 🤖 **Non-Interactive Mode** - Fallback to defaults for CI/CD environments, great for testing
|
|
33
|
+
- ✅ **Smart Validation** - Built-in pattern matching, custom validators, and sanitizers
|
|
34
|
+
- 🔀 **Conditional Logic** - Show/hide questions based on previous answers
|
|
35
|
+
- 🎨 **Interactive UX** - Fuzzy search, keyboard navigation, and visual feedback
|
|
36
|
+
- 🔄 **Dynamic Defaults** - Auto-populate defaults from git config, date/time, or custom resolvers
|
|
37
|
+
|
|
38
|
+
## Table of Contents
|
|
39
|
+
|
|
40
|
+
- [Quick Start](#quick-start)
|
|
41
|
+
- [Core Concepts](#core-concepts)
|
|
42
|
+
- [TypeScript Support](#typescript-support)
|
|
43
|
+
- [Question Types](#question-types)
|
|
44
|
+
- [Non-Interactive Mode](#non-interactive-mode)
|
|
45
|
+
- [API Reference](#api-reference)
|
|
46
|
+
- [Prompter Class](#genomic-class)
|
|
47
|
+
- [Question Types](#question-types-1)
|
|
48
|
+
- [Text Question](#text-question)
|
|
49
|
+
- [Number Question](#number-question)
|
|
50
|
+
- [Confirm Question](#confirm-question)
|
|
51
|
+
- [List Question](#list-question)
|
|
52
|
+
- [Autocomplete Question](#autocomplete-question)
|
|
53
|
+
- [Checkbox Question](#checkbox-question)
|
|
54
|
+
- [Advanced Question Options](#advanced-question-options)
|
|
55
|
+
- [Positional Arguments](#positional-arguments)
|
|
56
|
+
- [Real-World Examples](#real-world-examples)
|
|
57
|
+
- [Project Setup Wizard](#project-setup-wizard)
|
|
58
|
+
- [Configuration Builder](#configuration-builder)
|
|
59
|
+
- [CLI with Commander Integration](#cli-with-commander-integration)
|
|
60
|
+
- [Dynamic Dependencies](#dynamic-dependencies)
|
|
61
|
+
- [Custom Validation](#custom-validation)
|
|
62
|
+
- [Dynamic Defaults with Resolvers](#dynamic-defaults-with-resolvers)
|
|
63
|
+
- [Built-in Resolvers](#built-in-resolvers)
|
|
64
|
+
- [Custom Resolvers](#custom-resolvers)
|
|
65
|
+
- [Resolver Examples](#resolver-examples)
|
|
66
|
+
- [CLI Helper](#cli-helper)
|
|
67
|
+
- [Developing](#developing)
|
|
68
|
+
|
|
69
|
+
## Quick Start
|
|
70
|
+
|
|
71
|
+
```typescript
|
|
72
|
+
import { Prompter } from 'genomic';
|
|
73
|
+
|
|
74
|
+
const prompter = new Prompter();
|
|
75
|
+
|
|
76
|
+
const answers = await prompter.prompt({}, [
|
|
77
|
+
{
|
|
78
|
+
type: 'text',
|
|
79
|
+
name: 'username',
|
|
80
|
+
message: 'What is your username?',
|
|
81
|
+
required: true
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
type: 'confirm',
|
|
85
|
+
name: 'newsletter',
|
|
86
|
+
message: 'Subscribe to our newsletter?',
|
|
87
|
+
default: true
|
|
88
|
+
}
|
|
89
|
+
]);
|
|
90
|
+
|
|
91
|
+
console.log(answers);
|
|
92
|
+
// { username: 'john_doe', newsletter: true }
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Core Concepts
|
|
96
|
+
|
|
97
|
+
### TypeScript Support
|
|
98
|
+
|
|
99
|
+
Import types for full type safety:
|
|
100
|
+
|
|
101
|
+
```typescript
|
|
102
|
+
import {
|
|
103
|
+
Prompter,
|
|
104
|
+
Question,
|
|
105
|
+
TextQuestion,
|
|
106
|
+
NumberQuestion,
|
|
107
|
+
ConfirmQuestion,
|
|
108
|
+
ListQuestion,
|
|
109
|
+
AutocompleteQuestion,
|
|
110
|
+
CheckboxQuestion,
|
|
111
|
+
PrompterOptions,
|
|
112
|
+
DefaultResolverRegistry,
|
|
113
|
+
registerDefaultResolver,
|
|
114
|
+
resolveDefault
|
|
115
|
+
} from 'genomic';
|
|
116
|
+
|
|
117
|
+
interface UserConfig {
|
|
118
|
+
name: string;
|
|
119
|
+
age: number;
|
|
120
|
+
newsletter: boolean;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const answers = await prompter.prompt<UserConfig>({}, questions);
|
|
124
|
+
// answers is typed as UserConfig
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### Question Types
|
|
128
|
+
|
|
129
|
+
All questions support these base properties:
|
|
130
|
+
|
|
131
|
+
```typescript
|
|
132
|
+
interface BaseQuestion {
|
|
133
|
+
name: string; // Property name in result object
|
|
134
|
+
type: string; // Question type
|
|
135
|
+
_?: boolean; // Mark as positional argument (can be passed without --name flag)
|
|
136
|
+
message?: string; // Prompt message to display
|
|
137
|
+
description?: string; // Additional context
|
|
138
|
+
default?: any; // Default value
|
|
139
|
+
defaultFrom?: string; // Dynamic default from resolver (e.g., 'git.user.name')
|
|
140
|
+
setFrom?: string; // Auto-set value from resolver, bypassing prompt entirely
|
|
141
|
+
useDefault?: boolean; // Skip prompt and use default
|
|
142
|
+
required?: boolean; // Validation requirement
|
|
143
|
+
validate?: (input: any, answers: any) => boolean | Validation;
|
|
144
|
+
sanitize?: (input: any, answers: any) => any;
|
|
145
|
+
pattern?: string; // Regex pattern for validation
|
|
146
|
+
dependsOn?: string[]; // Question dependencies
|
|
147
|
+
when?: (answers: any) => boolean; // Conditional display
|
|
148
|
+
}
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### Non-Interactive Mode
|
|
152
|
+
|
|
153
|
+
When running in CI/CD or without a TTY, genomic automatically falls back to default values:
|
|
154
|
+
|
|
155
|
+
```typescript
|
|
156
|
+
const prompter = new Prompter({
|
|
157
|
+
noTty: true, // Force non-interactive mode
|
|
158
|
+
useDefaults: true // Use defaults without prompting
|
|
159
|
+
});
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
## API Reference
|
|
163
|
+
|
|
164
|
+
### Prompter Class
|
|
165
|
+
|
|
166
|
+
#### Constructor Options
|
|
167
|
+
|
|
168
|
+
```typescript
|
|
169
|
+
interface PrompterOptions {
|
|
170
|
+
noTty?: boolean; // Disable interactive mode
|
|
171
|
+
input?: Readable; // Input stream (default: process.stdin)
|
|
172
|
+
output?: Writable; // Output stream (default: process.stdout)
|
|
173
|
+
useDefaults?: boolean; // Skip prompts and use defaults
|
|
174
|
+
globalMaxLines?: number; // Max lines for list displays (default: 10)
|
|
175
|
+
mutateArgs?: boolean; // Mutate argv object (default: true)
|
|
176
|
+
resolverRegistry?: DefaultResolverRegistry; // Custom resolver registry
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const prompter = new Prompter(options);
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
#### Methods
|
|
183
|
+
|
|
184
|
+
```typescript
|
|
185
|
+
// Main prompt method
|
|
186
|
+
prompt<T>(argv: T, questions: Question[], options?: PromptOptions): Promise<T>
|
|
187
|
+
|
|
188
|
+
// Generate man page documentation
|
|
189
|
+
generateManPage(info: ManPageInfo): string
|
|
190
|
+
|
|
191
|
+
// Clean up resources
|
|
192
|
+
close(): void
|
|
193
|
+
exit(): void
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
#### Managing Multiple Instances
|
|
197
|
+
|
|
198
|
+
When working with multiple `Prompter` instances that share the same input stream (typically `process.stdin`), only one instance should be actively prompting at a time. Each instance attaches its own keyboard listener, so having multiple active instances will cause duplicate or unexpected keypress behavior.
|
|
199
|
+
|
|
200
|
+
**Best practices:**
|
|
201
|
+
|
|
202
|
+
1. **Reuse a single instance** - Create one `Prompter` instance and reuse it for all prompts:
|
|
203
|
+
```typescript
|
|
204
|
+
const prompter = new Prompter();
|
|
205
|
+
|
|
206
|
+
// Use the same instance for multiple prompt sessions
|
|
207
|
+
const answers1 = await prompter.prompt({}, questions1);
|
|
208
|
+
const answers2 = await prompter.prompt({}, questions2);
|
|
209
|
+
|
|
210
|
+
prompter.close(); // Clean up when done
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
2. **Close before creating another** - If you need separate instances, close the first before using the second:
|
|
214
|
+
```typescript
|
|
215
|
+
const prompter1 = new Prompter();
|
|
216
|
+
const answers1 = await prompter1.prompt({}, questions1);
|
|
217
|
+
prompter1.close(); // Important: close before creating another
|
|
218
|
+
|
|
219
|
+
const prompter2 = new Prompter();
|
|
220
|
+
const answers2 = await prompter2.prompt({}, questions2);
|
|
221
|
+
prompter2.close();
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
### Question Types
|
|
225
|
+
|
|
226
|
+
#### Text Question
|
|
227
|
+
|
|
228
|
+
Collect string input from users.
|
|
229
|
+
|
|
230
|
+
```typescript
|
|
231
|
+
{
|
|
232
|
+
type: 'text',
|
|
233
|
+
name: 'projectName',
|
|
234
|
+
message: 'What is your project name?',
|
|
235
|
+
default: 'my-app',
|
|
236
|
+
required: true,
|
|
237
|
+
pattern: '^[a-z0-9-]+$', // Regex validation
|
|
238
|
+
validate: (input) => {
|
|
239
|
+
if (input.length < 3) {
|
|
240
|
+
return { success: false, reason: 'Name must be at least 3 characters' };
|
|
241
|
+
}
|
|
242
|
+
return true;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
#### Number Question
|
|
248
|
+
|
|
249
|
+
Collect numeric input.
|
|
250
|
+
|
|
251
|
+
```typescript
|
|
252
|
+
{
|
|
253
|
+
type: 'number',
|
|
254
|
+
name: 'port',
|
|
255
|
+
message: 'Which port to use?',
|
|
256
|
+
default: 3000,
|
|
257
|
+
validate: (input) => {
|
|
258
|
+
if (input < 1 || input > 65535) {
|
|
259
|
+
return { success: false, reason: 'Port must be between 1 and 65535' };
|
|
260
|
+
}
|
|
261
|
+
return true;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
#### Confirm Question
|
|
267
|
+
|
|
268
|
+
Yes/no questions.
|
|
269
|
+
|
|
270
|
+
```typescript
|
|
271
|
+
{
|
|
272
|
+
type: 'confirm',
|
|
273
|
+
name: 'useTypeScript',
|
|
274
|
+
message: 'Use TypeScript?',
|
|
275
|
+
default: true // Default to 'yes'
|
|
276
|
+
}
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
#### List Question
|
|
280
|
+
|
|
281
|
+
Select one option from a list (no search).
|
|
282
|
+
|
|
283
|
+
```typescript
|
|
284
|
+
{
|
|
285
|
+
type: 'list',
|
|
286
|
+
name: 'license',
|
|
287
|
+
message: 'Choose a license',
|
|
288
|
+
options: ['MIT', 'Apache-2.0', 'GPL-3.0', 'BSD-3-Clause'],
|
|
289
|
+
default: 'MIT',
|
|
290
|
+
maxDisplayLines: 5
|
|
291
|
+
}
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
#### Autocomplete Question
|
|
295
|
+
|
|
296
|
+
Select with fuzzy search capabilities.
|
|
297
|
+
|
|
298
|
+
```typescript
|
|
299
|
+
{
|
|
300
|
+
type: 'autocomplete',
|
|
301
|
+
name: 'framework',
|
|
302
|
+
message: 'Choose your framework',
|
|
303
|
+
options: [
|
|
304
|
+
{ name: 'React', value: 'react' },
|
|
305
|
+
{ name: 'Vue.js', value: 'vue' },
|
|
306
|
+
{ name: 'Angular', value: 'angular' },
|
|
307
|
+
{ name: 'Svelte', value: 'svelte' }
|
|
308
|
+
],
|
|
309
|
+
allowCustomOptions: true, // Allow user to enter custom value
|
|
310
|
+
maxDisplayLines: 8
|
|
311
|
+
}
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
#### Checkbox Question
|
|
315
|
+
|
|
316
|
+
Multi-select with search.
|
|
317
|
+
|
|
318
|
+
```typescript
|
|
319
|
+
{
|
|
320
|
+
type: 'checkbox',
|
|
321
|
+
name: 'features',
|
|
322
|
+
message: 'Select features to include',
|
|
323
|
+
options: [
|
|
324
|
+
'Authentication',
|
|
325
|
+
'Database',
|
|
326
|
+
'API Routes',
|
|
327
|
+
'Testing',
|
|
328
|
+
'Documentation'
|
|
329
|
+
],
|
|
330
|
+
default: ['Authentication', 'API Routes'],
|
|
331
|
+
returnFullResults: false, // Only return selected items
|
|
332
|
+
required: true
|
|
333
|
+
}
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
With `returnFullResults: true`, returns all options with selection status:
|
|
337
|
+
|
|
338
|
+
```typescript
|
|
339
|
+
[
|
|
340
|
+
{ name: 'Authentication', value: 'Authentication', selected: true },
|
|
341
|
+
{ name: 'Database', value: 'Database', selected: false },
|
|
342
|
+
// ...
|
|
343
|
+
]
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
### Advanced Question Options
|
|
347
|
+
|
|
348
|
+
#### Custom Validation
|
|
349
|
+
|
|
350
|
+
```typescript
|
|
351
|
+
{
|
|
352
|
+
type: 'text',
|
|
353
|
+
name: 'email',
|
|
354
|
+
message: 'Enter your email',
|
|
355
|
+
pattern: '^[^@]+@[^@]+\\.[^@]+$',
|
|
356
|
+
validate: (email, answers) => {
|
|
357
|
+
// Custom async validation possible
|
|
358
|
+
if (email.endsWith('@example.com')) {
|
|
359
|
+
return {
|
|
360
|
+
success: false,
|
|
361
|
+
reason: 'Please use a real email address'
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
return { success: true };
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
#### Value Sanitization
|
|
370
|
+
|
|
371
|
+
```typescript
|
|
372
|
+
{
|
|
373
|
+
type: 'text',
|
|
374
|
+
name: 'tags',
|
|
375
|
+
message: 'Enter tags (comma-separated)',
|
|
376
|
+
sanitize: (input) => {
|
|
377
|
+
return input.split(',').map(tag => tag.trim());
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
#### Conditional Questions
|
|
383
|
+
|
|
384
|
+
```typescript
|
|
385
|
+
const questions: Question[] = [
|
|
386
|
+
{
|
|
387
|
+
type: 'confirm',
|
|
388
|
+
name: 'useDatabase',
|
|
389
|
+
message: 'Do you need a database?',
|
|
390
|
+
default: false
|
|
391
|
+
},
|
|
392
|
+
{
|
|
393
|
+
type: 'list',
|
|
394
|
+
name: 'database',
|
|
395
|
+
message: 'Which database?',
|
|
396
|
+
options: ['PostgreSQL', 'MySQL', 'MongoDB', 'SQLite'],
|
|
397
|
+
when: (answers) => answers.useDatabase === true // Only show if useDatabase is true
|
|
398
|
+
}
|
|
399
|
+
];
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
#### Question Dependencies
|
|
403
|
+
|
|
404
|
+
Ensure questions appear in the correct order:
|
|
405
|
+
|
|
406
|
+
```typescript
|
|
407
|
+
[
|
|
408
|
+
{
|
|
409
|
+
type: 'checkbox',
|
|
410
|
+
name: 'services',
|
|
411
|
+
message: 'Select services',
|
|
412
|
+
options: ['Auth', 'Storage', 'Functions']
|
|
413
|
+
},
|
|
414
|
+
{
|
|
415
|
+
type: 'text',
|
|
416
|
+
name: 'authProvider',
|
|
417
|
+
message: 'Which auth provider?',
|
|
418
|
+
dependsOn: ['services'], // Wait for services question
|
|
419
|
+
when: (answers) => {
|
|
420
|
+
const selected = answers.services.find(s => s.name === 'Auth');
|
|
421
|
+
return selected?.selected === true;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
]
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
### Positional Arguments
|
|
428
|
+
|
|
429
|
+
The `_` property allows you to name positional parameters, enabling users to pass values without flags. This is useful for CLI tools where the first few arguments have obvious meanings.
|
|
430
|
+
|
|
431
|
+
#### Basic Usage
|
|
432
|
+
|
|
433
|
+
```typescript
|
|
434
|
+
const questions: Question[] = [
|
|
435
|
+
{
|
|
436
|
+
_: true,
|
|
437
|
+
name: 'database',
|
|
438
|
+
type: 'text',
|
|
439
|
+
message: 'Database name',
|
|
440
|
+
required: true
|
|
441
|
+
}
|
|
442
|
+
];
|
|
443
|
+
|
|
444
|
+
const argv = minimist(process.argv.slice(2));
|
|
445
|
+
const result = await prompter.prompt(argv, questions);
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
Now users can run either:
|
|
449
|
+
```bash
|
|
450
|
+
node myprogram.js mydb1
|
|
451
|
+
# or equivalently:
|
|
452
|
+
node myprogram.js --database mydb1
|
|
453
|
+
```
|
|
454
|
+
|
|
455
|
+
#### Multiple Positional Arguments
|
|
456
|
+
|
|
457
|
+
Positional arguments are assigned in declaration order:
|
|
458
|
+
|
|
459
|
+
```typescript
|
|
460
|
+
const questions: Question[] = [
|
|
461
|
+
{ _: true, name: 'source', type: 'text', message: 'Source file' },
|
|
462
|
+
{ name: 'verbose', type: 'confirm', default: false },
|
|
463
|
+
{ _: true, name: 'destination', type: 'text', message: 'Destination file' }
|
|
464
|
+
];
|
|
465
|
+
|
|
466
|
+
// Running: node copy.js input.txt output.txt --verbose
|
|
467
|
+
// Results in: { source: 'input.txt', destination: 'output.txt', verbose: true }
|
|
468
|
+
```
|
|
469
|
+
|
|
470
|
+
#### Named Arguments Take Precedence
|
|
471
|
+
|
|
472
|
+
When both positional and named arguments are provided, named arguments win and the positional slot is preserved for the next positional question:
|
|
473
|
+
|
|
474
|
+
```typescript
|
|
475
|
+
const questions: Question[] = [
|
|
476
|
+
{ _: true, name: 'foo', type: 'text' },
|
|
477
|
+
{ _: true, name: 'bar', type: 'text' },
|
|
478
|
+
{ _: true, name: 'baz', type: 'text' }
|
|
479
|
+
];
|
|
480
|
+
|
|
481
|
+
// Running: node myprogram.js pos1 pos2 --bar named-bar
|
|
482
|
+
// Results in: { foo: 'pos1', bar: 'named-bar', baz: 'pos2' }
|
|
483
|
+
```
|
|
484
|
+
|
|
485
|
+
In this example, `bar` gets its value from the named flag, so the two positional values go to `foo` and `baz`.
|
|
486
|
+
|
|
487
|
+
#### Positional with Options
|
|
488
|
+
|
|
489
|
+
Positional arguments work with list, autocomplete, and checkbox questions. The value is mapped through the options:
|
|
490
|
+
|
|
491
|
+
```typescript
|
|
492
|
+
const questions: Question[] = [
|
|
493
|
+
{
|
|
494
|
+
_: true,
|
|
495
|
+
name: 'framework',
|
|
496
|
+
type: 'list',
|
|
497
|
+
options: [
|
|
498
|
+
{ name: 'React', value: 'react' },
|
|
499
|
+
{ name: 'Vue', value: 'vue' }
|
|
500
|
+
]
|
|
501
|
+
}
|
|
502
|
+
];
|
|
503
|
+
|
|
504
|
+
// Running: node setup.js React
|
|
505
|
+
// Results in: { framework: 'react' }
|
|
506
|
+
```
|
|
507
|
+
|
|
508
|
+
## Real-World Examples
|
|
509
|
+
|
|
510
|
+
### Project Setup Wizard
|
|
511
|
+
|
|
512
|
+
```typescript
|
|
513
|
+
import { Prompter, Question } from 'genomic';
|
|
514
|
+
import minimist from 'minimist';
|
|
515
|
+
|
|
516
|
+
const argv = minimist(process.argv.slice(2));
|
|
517
|
+
const prompter = new Prompter();
|
|
518
|
+
|
|
519
|
+
const questions: Question[] = [
|
|
520
|
+
{
|
|
521
|
+
type: 'text',
|
|
522
|
+
name: 'projectName',
|
|
523
|
+
message: 'Project name',
|
|
524
|
+
required: true,
|
|
525
|
+
pattern: '^[a-z0-9-]+$'
|
|
526
|
+
},
|
|
527
|
+
{
|
|
528
|
+
type: 'text',
|
|
529
|
+
name: 'description',
|
|
530
|
+
message: 'Project description',
|
|
531
|
+
default: 'My awesome project'
|
|
532
|
+
},
|
|
533
|
+
{
|
|
534
|
+
type: 'confirm',
|
|
535
|
+
name: 'typescript',
|
|
536
|
+
message: 'Use TypeScript?',
|
|
537
|
+
default: true
|
|
538
|
+
},
|
|
539
|
+
{
|
|
540
|
+
type: 'autocomplete',
|
|
541
|
+
name: 'framework',
|
|
542
|
+
message: 'Choose a framework',
|
|
543
|
+
options: ['React', 'Vue', 'Svelte', 'None'],
|
|
544
|
+
default: 'React'
|
|
545
|
+
},
|
|
546
|
+
{
|
|
547
|
+
type: 'checkbox',
|
|
548
|
+
name: 'tools',
|
|
549
|
+
message: 'Additional tools',
|
|
550
|
+
options: ['ESLint', 'Prettier', 'Jest', 'Husky'],
|
|
551
|
+
default: ['ESLint', 'Prettier']
|
|
552
|
+
}
|
|
553
|
+
];
|
|
554
|
+
|
|
555
|
+
const config = await prompter.prompt(argv, questions);
|
|
556
|
+
console.log('Creating project with:', config);
|
|
557
|
+
```
|
|
558
|
+
|
|
559
|
+
Run interactively:
|
|
560
|
+
```bash
|
|
561
|
+
node setup.js
|
|
562
|
+
```
|
|
563
|
+
|
|
564
|
+
Or with CLI args:
|
|
565
|
+
```bash
|
|
566
|
+
node setup.js --projectName=my-app --typescript --framework=React
|
|
567
|
+
```
|
|
568
|
+
|
|
569
|
+
### Configuration Builder
|
|
570
|
+
|
|
571
|
+
```typescript
|
|
572
|
+
interface AppConfig {
|
|
573
|
+
port: number;
|
|
574
|
+
host: string;
|
|
575
|
+
ssl: boolean;
|
|
576
|
+
sslCert?: string;
|
|
577
|
+
sslKey?: string;
|
|
578
|
+
database: string;
|
|
579
|
+
logLevel: string;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
const questions: Question[] = [
|
|
583
|
+
{
|
|
584
|
+
type: 'number',
|
|
585
|
+
name: 'port',
|
|
586
|
+
message: 'Server port',
|
|
587
|
+
default: 3000,
|
|
588
|
+
validate: (port) => port > 0 && port < 65536
|
|
589
|
+
},
|
|
590
|
+
{
|
|
591
|
+
type: 'text',
|
|
592
|
+
name: 'host',
|
|
593
|
+
message: 'Server host',
|
|
594
|
+
default: '0.0.0.0'
|
|
595
|
+
},
|
|
596
|
+
{
|
|
597
|
+
type: 'confirm',
|
|
598
|
+
name: 'ssl',
|
|
599
|
+
message: 'Enable SSL?',
|
|
600
|
+
default: false
|
|
601
|
+
},
|
|
602
|
+
{
|
|
603
|
+
type: 'text',
|
|
604
|
+
name: 'sslCert',
|
|
605
|
+
message: 'SSL certificate path',
|
|
606
|
+
when: (answers) => answers.ssl === true,
|
|
607
|
+
required: true
|
|
608
|
+
},
|
|
609
|
+
{
|
|
610
|
+
type: 'text',
|
|
611
|
+
name: 'sslKey',
|
|
612
|
+
message: 'SSL key path',
|
|
613
|
+
when: (answers) => answers.ssl === true,
|
|
614
|
+
required: true
|
|
615
|
+
},
|
|
616
|
+
{
|
|
617
|
+
type: 'list',
|
|
618
|
+
name: 'database',
|
|
619
|
+
message: 'Database type',
|
|
620
|
+
options: ['PostgreSQL', 'MySQL', 'SQLite'],
|
|
621
|
+
default: 'PostgreSQL'
|
|
622
|
+
},
|
|
623
|
+
{
|
|
624
|
+
type: 'list',
|
|
625
|
+
name: 'logLevel',
|
|
626
|
+
message: 'Log level',
|
|
627
|
+
options: ['error', 'warn', 'info', 'debug'],
|
|
628
|
+
default: 'info'
|
|
629
|
+
}
|
|
630
|
+
];
|
|
631
|
+
|
|
632
|
+
const config = await prompter.prompt<AppConfig>(argv, questions);
|
|
633
|
+
|
|
634
|
+
// Write config to file
|
|
635
|
+
fs.writeFileSync('config.json', JSON.stringify(config, null, 2));
|
|
636
|
+
```
|
|
637
|
+
|
|
638
|
+
### CLI with Commander Integration
|
|
639
|
+
|
|
640
|
+
```typescript
|
|
641
|
+
import { CLI, CommandHandler } from 'genomic';
|
|
642
|
+
import { Question } from 'genomic';
|
|
643
|
+
|
|
644
|
+
const handler: CommandHandler = async (argv, prompter, options) => {
|
|
645
|
+
const questions: Question[] = [
|
|
646
|
+
{
|
|
647
|
+
type: 'text',
|
|
648
|
+
name: 'name',
|
|
649
|
+
message: 'What is your name?',
|
|
650
|
+
required: true
|
|
651
|
+
},
|
|
652
|
+
{
|
|
653
|
+
type: 'number',
|
|
654
|
+
name: 'age',
|
|
655
|
+
message: 'What is your age?',
|
|
656
|
+
validate: (age) => age >= 0 && age <= 120
|
|
657
|
+
}
|
|
658
|
+
];
|
|
659
|
+
|
|
660
|
+
const answers = await prompter.prompt(argv, questions);
|
|
661
|
+
console.log('Hello,', answers.name);
|
|
662
|
+
};
|
|
663
|
+
|
|
664
|
+
const cli = new CLI(handler, {
|
|
665
|
+
version: 'myapp@1.0.0',
|
|
666
|
+
minimistOpts: {
|
|
667
|
+
alias: {
|
|
668
|
+
n: 'name',
|
|
669
|
+
a: 'age',
|
|
670
|
+
v: 'version'
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
});
|
|
674
|
+
|
|
675
|
+
await cli.run();
|
|
676
|
+
```
|
|
677
|
+
|
|
678
|
+
### Dynamic Dependencies
|
|
679
|
+
|
|
680
|
+
```typescript
|
|
681
|
+
const questions: Question[] = [
|
|
682
|
+
{
|
|
683
|
+
type: 'checkbox',
|
|
684
|
+
name: 'cloud',
|
|
685
|
+
message: 'Select cloud services',
|
|
686
|
+
options: ['AWS', 'Azure', 'GCP'],
|
|
687
|
+
returnFullResults: true
|
|
688
|
+
},
|
|
689
|
+
{
|
|
690
|
+
type: 'text',
|
|
691
|
+
name: 'awsRegion',
|
|
692
|
+
message: 'AWS Region',
|
|
693
|
+
dependsOn: ['cloud'],
|
|
694
|
+
when: (answers) => {
|
|
695
|
+
const aws = answers.cloud?.find(c => c.name === 'AWS');
|
|
696
|
+
return aws?.selected === true;
|
|
697
|
+
},
|
|
698
|
+
default: 'us-east-1'
|
|
699
|
+
},
|
|
700
|
+
{
|
|
701
|
+
type: 'text',
|
|
702
|
+
name: 'azureLocation',
|
|
703
|
+
message: 'Azure Location',
|
|
704
|
+
dependsOn: ['cloud'],
|
|
705
|
+
when: (answers) => {
|
|
706
|
+
const azure = answers.cloud?.find(c => c.name === 'Azure');
|
|
707
|
+
return azure?.selected === true;
|
|
708
|
+
},
|
|
709
|
+
default: 'eastus'
|
|
710
|
+
},
|
|
711
|
+
{
|
|
712
|
+
type: 'text',
|
|
713
|
+
name: 'gcpZone',
|
|
714
|
+
message: 'GCP Zone',
|
|
715
|
+
dependsOn: ['cloud'],
|
|
716
|
+
when: (answers) => {
|
|
717
|
+
const gcp = answers.cloud?.find(c => c.name === 'GCP');
|
|
718
|
+
return gcp?.selected === true;
|
|
719
|
+
},
|
|
720
|
+
default: 'us-central1-a'
|
|
721
|
+
}
|
|
722
|
+
];
|
|
723
|
+
|
|
724
|
+
const config = await prompter.prompt({}, questions);
|
|
725
|
+
```
|
|
726
|
+
|
|
727
|
+
### Custom Validation
|
|
728
|
+
|
|
729
|
+
```typescript
|
|
730
|
+
const questions: Question[] = [
|
|
731
|
+
{
|
|
732
|
+
type: 'text',
|
|
733
|
+
name: 'username',
|
|
734
|
+
message: 'Choose a username',
|
|
735
|
+
required: true,
|
|
736
|
+
pattern: '^[a-zA-Z0-9_]{3,20}$',
|
|
737
|
+
validate: async (username) => {
|
|
738
|
+
// Simulate API call to check availability
|
|
739
|
+
const available = await checkUsernameAvailability(username);
|
|
740
|
+
if (!available) {
|
|
741
|
+
return {
|
|
742
|
+
success: false,
|
|
743
|
+
reason: 'Username is already taken'
|
|
744
|
+
};
|
|
745
|
+
}
|
|
746
|
+
return { success: true };
|
|
747
|
+
}
|
|
748
|
+
},
|
|
749
|
+
{
|
|
750
|
+
type: 'text',
|
|
751
|
+
name: 'password',
|
|
752
|
+
message: 'Choose a password',
|
|
753
|
+
required: true,
|
|
754
|
+
validate: (password) => {
|
|
755
|
+
if (password.length < 8) {
|
|
756
|
+
return {
|
|
757
|
+
success: false,
|
|
758
|
+
reason: 'Password must be at least 8 characters'
|
|
759
|
+
};
|
|
760
|
+
}
|
|
761
|
+
if (!/[A-Z]/.test(password)) {
|
|
762
|
+
return {
|
|
763
|
+
success: false,
|
|
764
|
+
reason: 'Password must contain an uppercase letter'
|
|
765
|
+
};
|
|
766
|
+
}
|
|
767
|
+
if (!/[0-9]/.test(password)) {
|
|
768
|
+
return {
|
|
769
|
+
success: false,
|
|
770
|
+
reason: 'Password must contain a number'
|
|
771
|
+
};
|
|
772
|
+
}
|
|
773
|
+
return { success: true };
|
|
774
|
+
}
|
|
775
|
+
},
|
|
776
|
+
{
|
|
777
|
+
type: 'text',
|
|
778
|
+
name: 'confirmPassword',
|
|
779
|
+
message: 'Confirm password',
|
|
780
|
+
required: true,
|
|
781
|
+
dependsOn: ['password'],
|
|
782
|
+
validate: (confirm, answers) => {
|
|
783
|
+
if (confirm !== answers.password) {
|
|
784
|
+
return {
|
|
785
|
+
success: false,
|
|
786
|
+
reason: 'Passwords do not match'
|
|
787
|
+
};
|
|
788
|
+
}
|
|
789
|
+
return { success: true };
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
];
|
|
793
|
+
```
|
|
794
|
+
|
|
795
|
+
## Dynamic Defaults with Resolvers
|
|
796
|
+
|
|
797
|
+
The `defaultFrom` feature allows you to automatically populate question defaults from dynamic sources like git configuration, environment variables, date/time values, or custom resolvers. This eliminates repetitive boilerplate code for common default values.
|
|
798
|
+
|
|
799
|
+
### Quick Example
|
|
800
|
+
|
|
801
|
+
```typescript
|
|
802
|
+
import { Prompter } from 'genomic';
|
|
803
|
+
|
|
804
|
+
const questions = [
|
|
805
|
+
{
|
|
806
|
+
type: 'text',
|
|
807
|
+
name: 'authorName',
|
|
808
|
+
message: 'Author name?',
|
|
809
|
+
defaultFrom: 'git.user.name' // Auto-fills from git config
|
|
810
|
+
},
|
|
811
|
+
{
|
|
812
|
+
type: 'text',
|
|
813
|
+
name: 'authorEmail',
|
|
814
|
+
message: 'Author email?',
|
|
815
|
+
defaultFrom: 'git.user.email' // Auto-fills from git config
|
|
816
|
+
},
|
|
817
|
+
{
|
|
818
|
+
type: 'text',
|
|
819
|
+
name: 'npmUser',
|
|
820
|
+
message: 'NPM username?',
|
|
821
|
+
defaultFrom: 'npm.whoami' // Auto-fills from npm whoami
|
|
822
|
+
},
|
|
823
|
+
{
|
|
824
|
+
type: 'text',
|
|
825
|
+
name: 'copyrightYear',
|
|
826
|
+
message: 'Copyright year?',
|
|
827
|
+
defaultFrom: 'date.year' // Auto-fills current year
|
|
828
|
+
}
|
|
829
|
+
];
|
|
830
|
+
|
|
831
|
+
const prompter = new Prompter();
|
|
832
|
+
const answers = await prompter.prompt({}, questions);
|
|
833
|
+
```
|
|
834
|
+
|
|
835
|
+
### Built-in Resolvers
|
|
836
|
+
|
|
837
|
+
Prompter comes with several built-in resolvers ready to use:
|
|
838
|
+
|
|
839
|
+
#### Git Configuration
|
|
840
|
+
|
|
841
|
+
| Resolver | Description | Example Output |
|
|
842
|
+
|----------|-------------|----------------|
|
|
843
|
+
| `git.user.name` | Git global user name | `"John Doe"` |
|
|
844
|
+
| `git.user.email` | Git global user email | `"john@example.com"` |
|
|
845
|
+
|
|
846
|
+
#### NPM
|
|
847
|
+
|
|
848
|
+
| Resolver | Description | Example Output |
|
|
849
|
+
|----------|-------------|----------------|
|
|
850
|
+
| `npm.whoami` | Currently logged in npm user | `"johndoe"` |
|
|
851
|
+
|
|
852
|
+
#### Date & Time
|
|
853
|
+
|
|
854
|
+
| Resolver | Description | Example Output |
|
|
855
|
+
|----------|-------------|----------------|
|
|
856
|
+
| `date.year` | Current year | `"2025"` |
|
|
857
|
+
| `date.month` | Current month (zero-padded) | `"11"` |
|
|
858
|
+
| `date.day` | Current day (zero-padded) | `"23"` |
|
|
859
|
+
| `date.iso` | ISO date (YYYY-MM-DD) | `"2025-11-23"` |
|
|
860
|
+
| `date.now` | ISO timestamp | `"2025-11-23T15:30:45.123Z"` |
|
|
861
|
+
| `date.timestamp` | Unix timestamp (ms) | `"1732375845123"` |
|
|
862
|
+
|
|
863
|
+
#### Workspace (nearest package.json)
|
|
864
|
+
|
|
865
|
+
| Resolver | Description | Example Output |
|
|
866
|
+
|----------|-------------|----------------|
|
|
867
|
+
| `workspace.name` | Repo slug from `repository` URL (fallback: `package.json` `name`) | `"dev-utils"` |
|
|
868
|
+
| `workspace.repo.name` | Repo name from `repository` URL | `"dev-utils"` |
|
|
869
|
+
| `workspace.repo.organization` | Repo org/owner from `repository` URL | `"constructive-io"` |
|
|
870
|
+
| `workspace.organization.name` | Alias for `workspace.repo.organization` | `"constructive-io"` |
|
|
871
|
+
| `workspace.license` | License field from `package.json` | `"MIT"` |
|
|
872
|
+
| `workspace.author` | Author name from `package.json` | `"Constructive"` |
|
|
873
|
+
| `workspace.author.name` | Author name from `package.json` | `"Constructive"` |
|
|
874
|
+
| `workspace.author.email` | Author email from `package.json` | `"email@example.org"` |
|
|
875
|
+
|
|
876
|
+
### Priority Order
|
|
877
|
+
|
|
878
|
+
When resolving default values, genomic follows this priority:
|
|
879
|
+
|
|
880
|
+
1. **CLI Arguments** - Values passed via command line (highest priority)
|
|
881
|
+
2. **`setFrom`** - Auto-set values (bypasses prompt entirely)
|
|
882
|
+
3. **`defaultFrom`** - Dynamically resolved default values
|
|
883
|
+
4. **`default`** - Static default values
|
|
884
|
+
5. **`undefined`** - No default available
|
|
885
|
+
|
|
886
|
+
```typescript
|
|
887
|
+
{
|
|
888
|
+
type: 'text',
|
|
889
|
+
name: 'author',
|
|
890
|
+
defaultFrom: 'git.user.name', // Try git first
|
|
891
|
+
default: 'Anonymous' // Fallback if git not configured
|
|
892
|
+
}
|
|
893
|
+
```
|
|
894
|
+
|
|
895
|
+
### `setFrom` vs `defaultFrom`
|
|
896
|
+
|
|
897
|
+
Both `setFrom` and `defaultFrom` use resolvers to get values, but they behave differently:
|
|
898
|
+
|
|
899
|
+
| Feature | `defaultFrom` | `setFrom` |
|
|
900
|
+
|---------|---------------|-----------|
|
|
901
|
+
| Sets value as | Default (user can override) | Final value (no prompt) |
|
|
902
|
+
| User prompted? | Yes, with pre-filled default | No, question is skipped |
|
|
903
|
+
| Use case | Suggested values | Auto-computed values |
|
|
904
|
+
|
|
905
|
+
**`defaultFrom`** - The resolved value becomes the default, but the user is still prompted and can change it:
|
|
906
|
+
|
|
907
|
+
```typescript
|
|
908
|
+
{
|
|
909
|
+
type: 'text',
|
|
910
|
+
name: 'authorName',
|
|
911
|
+
message: 'Author name?',
|
|
912
|
+
defaultFrom: 'git.user.name' // User sees "Author name? [John Doe]" and can change it
|
|
913
|
+
}
|
|
914
|
+
```
|
|
915
|
+
|
|
916
|
+
**`setFrom`** - The resolved value is set directly and the question is skipped entirely:
|
|
917
|
+
|
|
918
|
+
```typescript
|
|
919
|
+
{
|
|
920
|
+
type: 'text',
|
|
921
|
+
name: 'year',
|
|
922
|
+
message: 'Copyright year?',
|
|
923
|
+
setFrom: 'date.year' // Automatically set to "2025", no prompt shown
|
|
924
|
+
}
|
|
925
|
+
```
|
|
926
|
+
|
|
927
|
+
#### When to use each
|
|
928
|
+
|
|
929
|
+
Use `defaultFrom` when:
|
|
930
|
+
- The value is a suggestion the user might want to change
|
|
931
|
+
- User confirmation is desired
|
|
932
|
+
|
|
933
|
+
Use `setFrom` when:
|
|
934
|
+
- The value should be computed automatically
|
|
935
|
+
- No user input is needed (e.g., timestamps, computed fields)
|
|
936
|
+
- You want to reduce the number of prompts
|
|
937
|
+
|
|
938
|
+
#### Combined example
|
|
939
|
+
|
|
940
|
+
```typescript
|
|
941
|
+
const questions = [
|
|
942
|
+
{
|
|
943
|
+
type: 'text',
|
|
944
|
+
name: 'authorName',
|
|
945
|
+
message: 'Author name?',
|
|
946
|
+
defaultFrom: 'git.user.name' // User can override
|
|
947
|
+
},
|
|
948
|
+
{
|
|
949
|
+
type: 'text',
|
|
950
|
+
name: 'createdAt',
|
|
951
|
+
setFrom: 'date.iso' // Auto-set, no prompt
|
|
952
|
+
},
|
|
953
|
+
{
|
|
954
|
+
type: 'text',
|
|
955
|
+
name: 'copyrightYear',
|
|
956
|
+
setFrom: 'date.year' // Auto-set, no prompt
|
|
957
|
+
}
|
|
958
|
+
];
|
|
959
|
+
|
|
960
|
+
// User only sees prompt for authorName
|
|
961
|
+
// createdAt and copyrightYear are set automatically
|
|
962
|
+
```
|
|
963
|
+
|
|
964
|
+
### Custom Resolvers
|
|
965
|
+
|
|
966
|
+
Register your own custom resolvers for project-specific needs:
|
|
967
|
+
|
|
968
|
+
```typescript
|
|
969
|
+
import { registerDefaultResolver } from 'genomic';
|
|
970
|
+
|
|
971
|
+
// Register a resolver for current directory name
|
|
972
|
+
registerDefaultResolver('cwd.name', () => {
|
|
973
|
+
return process.cwd().split('/').pop();
|
|
974
|
+
});
|
|
975
|
+
|
|
976
|
+
// Register a resolver for environment variable
|
|
977
|
+
registerDefaultResolver('env.user', () => {
|
|
978
|
+
return process.env.USER;
|
|
979
|
+
});
|
|
980
|
+
|
|
981
|
+
// Use in questions
|
|
982
|
+
const questions = [
|
|
983
|
+
{
|
|
984
|
+
type: 'text',
|
|
985
|
+
name: 'projectName',
|
|
986
|
+
message: 'Project name?',
|
|
987
|
+
defaultFrom: 'cwd.name',
|
|
988
|
+
default: 'my-project'
|
|
989
|
+
},
|
|
990
|
+
{
|
|
991
|
+
type: 'text',
|
|
992
|
+
name: 'author',
|
|
993
|
+
message: 'Author?',
|
|
994
|
+
defaultFrom: 'env.user'
|
|
995
|
+
}
|
|
996
|
+
];
|
|
997
|
+
```
|
|
998
|
+
|
|
999
|
+
### Instance-Specific Resolvers
|
|
1000
|
+
|
|
1001
|
+
For isolated resolver registries, use a custom resolver registry per Prompter instance:
|
|
1002
|
+
|
|
1003
|
+
```typescript
|
|
1004
|
+
import { DefaultResolverRegistry, Prompter } from 'genomic';
|
|
1005
|
+
|
|
1006
|
+
const customRegistry = new DefaultResolverRegistry();
|
|
1007
|
+
|
|
1008
|
+
// Register resolvers specific to this instance
|
|
1009
|
+
customRegistry.register('app.name', () => 'my-app');
|
|
1010
|
+
customRegistry.register('app.port', () => 3000);
|
|
1011
|
+
|
|
1012
|
+
const prompter = new Prompter({
|
|
1013
|
+
resolverRegistry: customRegistry // Use custom registry
|
|
1014
|
+
});
|
|
1015
|
+
|
|
1016
|
+
const questions = [
|
|
1017
|
+
{
|
|
1018
|
+
type: 'text',
|
|
1019
|
+
name: 'appName',
|
|
1020
|
+
defaultFrom: 'app.name'
|
|
1021
|
+
},
|
|
1022
|
+
{
|
|
1023
|
+
type: 'number',
|
|
1024
|
+
name: 'port',
|
|
1025
|
+
defaultFrom: 'app.port'
|
|
1026
|
+
}
|
|
1027
|
+
];
|
|
1028
|
+
|
|
1029
|
+
const answers = await prompter.prompt({}, questions);
|
|
1030
|
+
```
|
|
1031
|
+
|
|
1032
|
+
### Resolver Examples
|
|
1033
|
+
|
|
1034
|
+
#### System Information
|
|
1035
|
+
|
|
1036
|
+
```typescript
|
|
1037
|
+
import os from 'os';
|
|
1038
|
+
import { registerDefaultResolver } from 'genomic';
|
|
1039
|
+
|
|
1040
|
+
registerDefaultResolver('system.hostname', () => os.hostname());
|
|
1041
|
+
registerDefaultResolver('system.username', () => os.userInfo().username);
|
|
1042
|
+
|
|
1043
|
+
const questions = [
|
|
1044
|
+
{
|
|
1045
|
+
type: 'text',
|
|
1046
|
+
name: 'hostname',
|
|
1047
|
+
message: 'Hostname?',
|
|
1048
|
+
defaultFrom: 'system.hostname'
|
|
1049
|
+
}
|
|
1050
|
+
];
|
|
1051
|
+
```
|
|
1052
|
+
|
|
1053
|
+
#### Conditional Defaults
|
|
1054
|
+
|
|
1055
|
+
```typescript
|
|
1056
|
+
registerDefaultResolver('app.port', () => {
|
|
1057
|
+
return process.env.NODE_ENV === 'production' ? 80 : 3000;
|
|
1058
|
+
});
|
|
1059
|
+
|
|
1060
|
+
const questions = [
|
|
1061
|
+
{
|
|
1062
|
+
type: 'number',
|
|
1063
|
+
name: 'port',
|
|
1064
|
+
message: 'Port?',
|
|
1065
|
+
defaultFrom: 'app.port'
|
|
1066
|
+
}
|
|
1067
|
+
];
|
|
1068
|
+
```
|
|
1069
|
+
|
|
1070
|
+
### Error Handling
|
|
1071
|
+
|
|
1072
|
+
Resolvers fail silently by default. If a resolver throws an error or returns `undefined`, genomic falls back to the static `default` value (if provided):
|
|
1073
|
+
|
|
1074
|
+
```typescript
|
|
1075
|
+
{
|
|
1076
|
+
type: 'text',
|
|
1077
|
+
name: 'author',
|
|
1078
|
+
defaultFrom: 'git.user.name', // May fail if git not configured
|
|
1079
|
+
default: 'Anonymous', // Used if resolver fails
|
|
1080
|
+
required: true
|
|
1081
|
+
}
|
|
1082
|
+
```
|
|
1083
|
+
|
|
1084
|
+
For debugging, set `DEBUG=genomic` to see resolver errors:
|
|
1085
|
+
|
|
1086
|
+
```bash
|
|
1087
|
+
DEBUG=genomic node your-cli.js
|
|
1088
|
+
```
|
|
1089
|
+
|
|
1090
|
+
### Real-World Use Case
|
|
1091
|
+
|
|
1092
|
+
```typescript
|
|
1093
|
+
import { Prompter, registerDefaultResolver } from 'genomic';
|
|
1094
|
+
|
|
1095
|
+
// Register a resolver for current directory name
|
|
1096
|
+
registerDefaultResolver('cwd.name', () => {
|
|
1097
|
+
return process.cwd().split('/').pop();
|
|
1098
|
+
});
|
|
1099
|
+
|
|
1100
|
+
const questions = [
|
|
1101
|
+
{
|
|
1102
|
+
type: 'text',
|
|
1103
|
+
name: 'projectName',
|
|
1104
|
+
message: 'Project name?',
|
|
1105
|
+
defaultFrom: 'cwd.name',
|
|
1106
|
+
required: true
|
|
1107
|
+
},
|
|
1108
|
+
{
|
|
1109
|
+
type: 'text',
|
|
1110
|
+
name: 'author',
|
|
1111
|
+
message: 'Author?',
|
|
1112
|
+
defaultFrom: 'git.user.name',
|
|
1113
|
+
required: true
|
|
1114
|
+
},
|
|
1115
|
+
{
|
|
1116
|
+
type: 'text',
|
|
1117
|
+
name: 'email',
|
|
1118
|
+
message: 'Email?',
|
|
1119
|
+
defaultFrom: 'git.user.email',
|
|
1120
|
+
required: true
|
|
1121
|
+
},
|
|
1122
|
+
{
|
|
1123
|
+
type: 'text',
|
|
1124
|
+
name: 'year',
|
|
1125
|
+
message: 'Copyright year?',
|
|
1126
|
+
defaultFrom: 'date.year'
|
|
1127
|
+
}
|
|
1128
|
+
];
|
|
1129
|
+
|
|
1130
|
+
const prompter = new Prompter();
|
|
1131
|
+
const config = await prompter.prompt({}, questions);
|
|
1132
|
+
```
|
|
1133
|
+
|
|
1134
|
+
With git configured, the prompts will show:
|
|
1135
|
+
|
|
1136
|
+
```bash
|
|
1137
|
+
Project name? (my-project-dir)
|
|
1138
|
+
Author? (John Doe)
|
|
1139
|
+
Email? (john@example.com)
|
|
1140
|
+
Copyright year? (2025)
|
|
1141
|
+
```
|
|
1142
|
+
|
|
1143
|
+
All defaults automatically populated from git config, directory name, and current date!
|
|
1144
|
+
|
|
1145
|
+
## CLI Helper
|
|
1146
|
+
|
|
1147
|
+
The `CLI` class provides integration with command-line argument parsing:
|
|
1148
|
+
|
|
1149
|
+
```typescript
|
|
1150
|
+
import { CLI, CommandHandler, CLIOptions } from 'genomic';
|
|
1151
|
+
|
|
1152
|
+
const options: Partial<CLIOptions> = {
|
|
1153
|
+
version: 'myapp@1.0.0',
|
|
1154
|
+
minimistOpts: {
|
|
1155
|
+
alias: {
|
|
1156
|
+
v: 'version',
|
|
1157
|
+
h: 'help'
|
|
1158
|
+
},
|
|
1159
|
+
boolean: ['help', 'version'],
|
|
1160
|
+
string: ['name', 'output']
|
|
1161
|
+
}
|
|
1162
|
+
};
|
|
1163
|
+
|
|
1164
|
+
const handler: CommandHandler = async (argv, prompter) => {
|
|
1165
|
+
if (argv.help) {
|
|
1166
|
+
console.log('Usage: myapp [options]');
|
|
1167
|
+
process.exit(0);
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
const answers = await prompter.prompt(argv, questions);
|
|
1171
|
+
// Handle answers
|
|
1172
|
+
};
|
|
1173
|
+
|
|
1174
|
+
const cli = new CLI(handler, options);
|
|
1175
|
+
await cli.run();
|
|
1176
|
+
```
|
|
1177
|
+
|
|
1178
|
+
---
|
|
1179
|
+
|
|
1180
|
+
## Development
|
|
1181
|
+
|
|
1182
|
+
### Setup
|
|
1183
|
+
|
|
1184
|
+
1. Clone the repository:
|
|
1185
|
+
|
|
1186
|
+
```bash
|
|
1187
|
+
git clone https://github.com/constructive-io/dev-utils.git
|
|
1188
|
+
```
|
|
1189
|
+
|
|
1190
|
+
2. Install dependencies:
|
|
1191
|
+
|
|
1192
|
+
```bash
|
|
1193
|
+
cd dev-utils
|
|
1194
|
+
pnpm install
|
|
1195
|
+
pnpm build
|
|
1196
|
+
```
|
|
1197
|
+
|
|
1198
|
+
3. Test the package of interest:
|
|
1199
|
+
|
|
1200
|
+
```bash
|
|
1201
|
+
cd packages/<packagename>
|
|
1202
|
+
pnpm test:watch
|
|
1203
|
+
```
|
|
1204
|
+
|
|
1205
|
+
## Credits
|
|
1206
|
+
|
|
1207
|
+
Built for developers, with developers.
|
|
1208
|
+
👉 https://launchql.com | https://hyperweb.io
|
|
1209
|
+
|
|
1210
|
+
## Disclaimer
|
|
1211
|
+
|
|
1212
|
+
AS DESCRIBED IN THE LICENSES, THE SOFTWARE IS PROVIDED "AS IS", AT YOUR OWN RISK, AND WITHOUT WARRANTIES OF ANY KIND.
|
|
1213
|
+
|
|
1214
|
+
No developer or entity involved in creating this software will be liable for any claims or damages whatsoever associated with your use, inability to use, or your interaction with other users of the code, including any direct, indirect, incidental, special, exemplary, punitive or consequential damages, or loss of profits, cryptocurrencies, tokens, or anything else of value.
|