genomic 0.0.1 → 4.0.0

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/README.md CHANGED
@@ -1 +1,1214 @@
1
- # genome
1
+ # genomic
2
+
3
+ <p align="center" width="100%">
4
+ <img height="90" src="https://raw.githubusercontent.com/hyperweb-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/hyperweb-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.