Skip to content

Commit 4f96a88

Browse files
authored
Merge pull request #12 from hyperweb-io/features
Features
2 parents ce74d19 + 2797a99 commit 4f96a88

File tree

10 files changed

+1298
-2
lines changed

10 files changed

+1298
-2
lines changed

packages/inquirerer/__tests__/defaultFrom.test.ts

Lines changed: 554 additions & 0 deletions
Large diffs are not rendered by default.

packages/inquirerer/__tests__/resolvers.test.ts

Lines changed: 424 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import { Inquirerer, registerDefaultResolver } from '../src';
2+
3+
/**
4+
* Example demonstrating the new defaultFrom feature
5+
*
6+
* This feature allows questions to automatically populate defaults from:
7+
* - Git configuration (git.user.name, git.user.email)
8+
* - Date/time values (date.year, date.iso, date.now)
9+
* - Custom resolvers you register
10+
*/
11+
12+
async function basicExample() {
13+
console.log('=== Basic Example: Git and Date Resolvers ===\n');
14+
15+
const questions = [
16+
{
17+
name: 'authorName',
18+
type: 'text' as const,
19+
message: 'Author name?',
20+
defaultFrom: 'git.user.name' // Auto-fills from git config
21+
},
22+
{
23+
name: 'authorEmail',
24+
type: 'text' as const,
25+
message: 'Author email?',
26+
defaultFrom: 'git.user.email' // Auto-fills from git config
27+
},
28+
{
29+
name: 'year',
30+
type: 'text' as const,
31+
message: 'Copyright year?',
32+
defaultFrom: 'date.year' // Auto-fills current year
33+
}
34+
];
35+
36+
const prompter = new Inquirerer();
37+
const answers = await prompter.prompt({}, questions);
38+
39+
console.log('\nAnswers:', answers);
40+
prompter.close();
41+
}
42+
43+
async function customResolverExample() {
44+
console.log('\n=== Custom Resolver Example ===\n');
45+
46+
// Register custom resolvers
47+
registerDefaultResolver('package.name', async () => {
48+
// In a real app, you'd read from package.json
49+
return 'my-awesome-package';
50+
});
51+
52+
registerDefaultResolver('cwd.name', () => {
53+
return process.cwd().split('/').pop();
54+
});
55+
56+
const questions = [
57+
{
58+
name: 'pkgName',
59+
type: 'text' as const,
60+
message: 'Package name?',
61+
defaultFrom: 'package.name' // Uses custom resolver
62+
},
63+
{
64+
name: 'dirName',
65+
type: 'text' as const,
66+
message: 'Directory name?',
67+
defaultFrom: 'cwd.name' // Uses custom resolver
68+
}
69+
];
70+
71+
const prompter = new Inquirerer();
72+
const answers = await prompter.prompt({}, questions);
73+
74+
console.log('\nAnswers:', answers);
75+
prompter.close();
76+
}
77+
78+
async function allAvailableResolvers() {
79+
console.log('\n=== All Built-in Resolvers ===\n');
80+
81+
const builtInResolvers = [
82+
'git.user.name',
83+
'git.user.email',
84+
'date.year',
85+
'date.month',
86+
'date.day',
87+
'date.now',
88+
'date.iso',
89+
'date.timestamp'
90+
];
91+
92+
console.log('Built-in resolvers available:');
93+
builtInResolvers.forEach(resolver => {
94+
console.log(` - ${resolver}`);
95+
});
96+
}
97+
98+
// Run examples
99+
(async () => {
100+
await allAvailableResolvers();
101+
// Uncomment to run interactive examples:
102+
// await basicExample();
103+
// await customResolverExample();
104+
})();

packages/inquirerer/src/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
export * from './commander';
22
export * from './prompt';
3-
export * from './question';
3+
export * from './question';
4+
export * from './resolvers';

packages/inquirerer/src/prompt.ts

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { Readable, Writable } from 'stream';
44

55
import { KEY_CODES, TerminalKeypress } from './keypress';
66
import { AutocompleteQuestion, CheckboxQuestion, ConfirmQuestion, ListQuestion, NumberQuestion, OptionValue, Question, TextQuestion, Validation, Value } from './question';
7+
import { DefaultResolverRegistry, globalResolverRegistry } from './resolvers';
78
// import { writeFileSync } from 'fs';
89

910
// const debuglog = (obj: any) => {
@@ -250,6 +251,7 @@ export interface InquirererOptions {
250251
useDefaults?: boolean;
251252
globalMaxLines?: number;
252253
mutateArgs?: boolean;
254+
resolverRegistry?: DefaultResolverRegistry;
253255

254256
}
255257
export class Inquirerer {
@@ -261,6 +263,7 @@ export class Inquirerer {
261263
private useDefaults: boolean;
262264
private globalMaxLines: number;
263265
private mutateArgs: boolean;
266+
private resolverRegistry: DefaultResolverRegistry;
264267

265268
private handledKeys: Set<string> = new Set();
266269

@@ -273,7 +276,8 @@ export class Inquirerer {
273276
output = process.stdout,
274277
useDefaults = false,
275278
globalMaxLines = 10,
276-
mutateArgs = true
279+
mutateArgs = true,
280+
resolverRegistry = globalResolverRegistry
277281
} = options ?? {}
278282

279283
this.useDefaults = useDefaults;
@@ -282,6 +286,7 @@ export class Inquirerer {
282286
this.mutateArgs = mutateArgs;
283287
this.input = input;
284288
this.globalMaxLines = globalMaxLines;
289+
this.resolverRegistry = resolverRegistry;
285290

286291
if (!noTty) {
287292
this.rl = readline.createInterface({
@@ -461,6 +466,9 @@ export class Inquirerer {
461466
const shouldMutate = options?.mutateArgs !== undefined ? options.mutateArgs : this.mutateArgs;
462467
let obj: any = shouldMutate ? argv : { ...argv };
463468

469+
// Resolve dynamic defaults before processing questions
470+
await this.resolveDynamicDefaults(questions);
471+
464472
// first loop through the question, and set any overrides in case other questions use objs for validation
465473
this.applyOverrides(argv, obj, questions);
466474

@@ -543,6 +551,43 @@ export class Inquirerer {
543551
return questions.some(question => question.required && this.isEmptyAnswer(argv[question.name]));
544552
}
545553

554+
/**
555+
* Resolves the default value for a question using the resolver system.
556+
* Priority: defaultFrom > default > undefined
557+
*/
558+
private async resolveQuestionDefault(question: Question): Promise<any> {
559+
// Try to resolve from defaultFrom first
560+
if ('defaultFrom' in question && question.defaultFrom) {
561+
const resolved = await this.resolverRegistry.resolve(question.defaultFrom);
562+
if (resolved !== undefined) {
563+
return resolved;
564+
}
565+
}
566+
567+
// Fallback to static default
568+
if ('default' in question) {
569+
return question.default;
570+
}
571+
572+
return undefined;
573+
}
574+
575+
/**
576+
* Resolves dynamic defaults for all questions that have defaultFrom specified.
577+
* Updates the question.default property with the resolved value.
578+
*/
579+
private async resolveDynamicDefaults(questions: Question[]): Promise<void> {
580+
for (const question of questions) {
581+
if ('defaultFrom' in question && question.defaultFrom) {
582+
const resolved = await this.resolveQuestionDefault(question);
583+
if (resolved !== undefined) {
584+
// Update question.default with resolved value
585+
(question as any).default = resolved;
586+
}
587+
}
588+
}
589+
}
590+
546591
private applyDefaultValues(questions: Question[], obj: any): void {
547592
questions.forEach(question => {
548593
if ('default' in question) {

packages/inquirerer/src/question/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ export interface BaseQuestion {
1818
name: string;
1919
type: string;
2020
default?: any;
21+
defaultFrom?: string;
2122
useDefault?: boolean;
2223
required?: boolean;
2324
message?: string;
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import type { ResolverRegistry } from './types';
2+
3+
/**
4+
* Built-in date/time resolvers.
5+
*/
6+
export const dateResolvers: ResolverRegistry = {
7+
'date.year': () => new Date().getFullYear().toString(),
8+
'date.month': () => (new Date().getMonth() + 1).toString().padStart(2, '0'),
9+
'date.day': () => new Date().getDate().toString().padStart(2, '0'),
10+
'date.now': () => new Date().toISOString(),
11+
'date.iso': () => new Date().toISOString().split('T')[0], // YYYY-MM-DD
12+
'date.timestamp': () => Date.now().toString(),
13+
};
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { execSync } from 'child_process';
2+
import type { ResolverRegistry } from './types';
3+
4+
/**
5+
* Retrieves a git config value.
6+
* @param key The git config key (e.g., 'user.name', 'user.email')
7+
* @returns The config value as a string, or undefined if not found or error occurs
8+
*/
9+
export function getGitConfig(key: string): string | undefined {
10+
try {
11+
const result = execSync(`git config --global ${key}`, {
12+
encoding: 'utf8',
13+
stdio: ['pipe', 'pipe', 'ignore'] // Suppress stderr
14+
});
15+
const trimmed = result.trim();
16+
return trimmed || undefined; // Treat empty string as undefined
17+
} catch {
18+
return undefined;
19+
}
20+
}
21+
22+
/**
23+
* Built-in git configuration resolvers.
24+
*/
25+
export const gitResolvers: ResolverRegistry = {
26+
'git.user.name': () => getGitConfig('user.name'),
27+
'git.user.email': () => getGitConfig('user.email'),
28+
};
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
import { gitResolvers } from './git';
2+
import { dateResolvers } from './date';
3+
import type { DefaultResolver, ResolverRegistry } from './types';
4+
5+
/**
6+
* A registry for managing default value resolvers.
7+
* Allows registration of custom resolvers and provides resolution logic.
8+
*/
9+
export class DefaultResolverRegistry {
10+
private resolvers: ResolverRegistry;
11+
12+
constructor(initialResolvers: ResolverRegistry = {}) {
13+
this.resolvers = { ...initialResolvers };
14+
}
15+
16+
/**
17+
* Register a custom resolver.
18+
* @param key The resolver key (e.g., 'git.user.name')
19+
* @param resolver The resolver function
20+
*/
21+
register(key: string, resolver: DefaultResolver): void {
22+
this.resolvers[key] = resolver;
23+
}
24+
25+
/**
26+
* Unregister a resolver.
27+
* @param key The resolver key to remove
28+
*/
29+
unregister(key: string): void {
30+
delete this.resolvers[key];
31+
}
32+
33+
/**
34+
* Resolve a key to its value.
35+
* Returns undefined if the resolver doesn't exist or if it throws an error.
36+
* @param key The resolver key
37+
* @returns The resolved value or undefined
38+
*/
39+
async resolve(key: string): Promise<any> {
40+
const resolver = this.resolvers[key];
41+
if (!resolver) {
42+
return undefined;
43+
}
44+
45+
try {
46+
const result = await Promise.resolve(resolver());
47+
// Treat empty strings as undefined
48+
return result === '' ? undefined : result;
49+
} catch (error) {
50+
// Silent failure - log only in debug mode
51+
if (process.env.DEBUG === 'inquirerer') {
52+
console.error(`[inquirerer] Resolver '${key}' failed:`, error);
53+
}
54+
return undefined;
55+
}
56+
}
57+
58+
/**
59+
* Check if a resolver exists for the given key.
60+
* @param key The resolver key
61+
* @returns True if the resolver exists
62+
*/
63+
has(key: string): boolean {
64+
return key in this.resolvers;
65+
}
66+
67+
/**
68+
* Get all registered resolver keys.
69+
* @returns Array of resolver keys
70+
*/
71+
keys(): string[] {
72+
return Object.keys(this.resolvers);
73+
}
74+
75+
/**
76+
* Create a copy of this registry with all current resolvers.
77+
* @returns A new DefaultResolverRegistry instance
78+
*/
79+
clone(): DefaultResolverRegistry {
80+
return new DefaultResolverRegistry({ ...this.resolvers });
81+
}
82+
}
83+
84+
/**
85+
* Global resolver registry instance with built-in resolvers.
86+
* This is the default registry used by Inquirerer unless a custom one is provided.
87+
*/
88+
export const globalResolverRegistry = new DefaultResolverRegistry({
89+
...gitResolvers,
90+
...dateResolvers,
91+
});
92+
93+
/**
94+
* Convenience function to register a resolver on the global registry.
95+
* @param key The resolver key
96+
* @param resolver The resolver function
97+
*/
98+
export function registerDefaultResolver(key: string, resolver: DefaultResolver): void {
99+
globalResolverRegistry.register(key, resolver);
100+
}
101+
102+
/**
103+
* Convenience function to resolve a key using the global registry.
104+
* @param key The resolver key
105+
* @returns The resolved value or undefined
106+
*/
107+
export function resolveDefault(key: string): Promise<any> {
108+
return globalResolverRegistry.resolve(key);
109+
}
110+
111+
// Re-export types and utilities
112+
export type { DefaultResolver, ResolverRegistry } from './types';
113+
export { getGitConfig } from './git';
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
/**
2+
* A function that resolves a default value dynamically.
3+
* Can be synchronous or asynchronous.
4+
*/
5+
export type DefaultResolver = () => Promise<any> | any;
6+
7+
/**
8+
* A registry of resolver functions, keyed by their resolver name.
9+
* Example: { 'git.user.name': () => getGitConfig('user.name') }
10+
*/
11+
export interface ResolverRegistry {
12+
[key: string]: DefaultResolver;
13+
}

0 commit comments

Comments
 (0)