genomic 4.0.1 → 5.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 +153 -1127
- package/cache/cache-manager.d.ts +60 -0
- package/cache/cache-manager.js +228 -0
- package/cache/types.d.ts +22 -0
- package/esm/cache/cache-manager.js +191 -0
- package/esm/git/git-cloner.js +92 -0
- package/esm/index.js +41 -4
- package/esm/licenses.js +120 -0
- package/esm/scaffolder/index.js +2 -0
- package/esm/scaffolder/template-scaffolder.js +310 -0
- package/esm/scaffolder/types.js +1 -0
- package/esm/template/extract.js +162 -0
- package/esm/template/prompt.js +103 -0
- package/esm/template/replace.js +110 -0
- package/esm/template/templatizer.js +73 -0
- package/esm/template/types.js +1 -0
- package/esm/types.js +1 -0
- package/esm/utils/npm-version-check.js +52 -0
- package/esm/utils/types.js +1 -0
- package/git/git-cloner.d.ts +32 -0
- package/git/git-cloner.js +129 -0
- package/git/types.d.ts +15 -0
- package/index.d.ts +29 -4
- package/index.js +43 -4
- package/licenses-templates/APACHE-2.0.txt +18 -0
- package/licenses-templates/BSD-3-CLAUSE.txt +28 -0
- package/licenses-templates/CLOSED.txt +20 -0
- package/licenses-templates/GPL-3.0.txt +18 -0
- package/licenses-templates/ISC.txt +16 -0
- package/licenses-templates/MIT.txt +22 -0
- package/licenses-templates/MPL-2.0.txt +8 -0
- package/licenses-templates/UNLICENSE.txt +22 -0
- package/licenses.d.ts +18 -0
- package/licenses.js +162 -0
- package/package.json +9 -14
- package/scaffolder/index.d.ts +2 -0
- package/{question → scaffolder}/index.js +1 -0
- package/scaffolder/template-scaffolder.d.ts +91 -0
- package/scaffolder/template-scaffolder.js +347 -0
- package/scaffolder/types.d.ts +191 -0
- package/scaffolder/types.js +2 -0
- package/template/extract.d.ts +7 -0
- package/template/extract.js +198 -0
- package/template/prompt.d.ts +19 -0
- package/template/prompt.js +107 -0
- package/template/replace.d.ts +9 -0
- package/template/replace.js +146 -0
- package/template/templatizer.d.ts +33 -0
- package/template/templatizer.js +110 -0
- package/template/types.d.ts +18 -0
- package/template/types.js +2 -0
- package/types.d.ts +99 -0
- package/types.js +2 -0
- package/utils/npm-version-check.d.ts +17 -0
- package/utils/npm-version-check.js +57 -0
- package/utils/types.d.ts +6 -0
- package/utils/types.js +2 -0
- package/commander.d.ts +0 -21
- package/commander.js +0 -57
- package/esm/commander.js +0 -50
- package/esm/keypress.js +0 -95
- package/esm/prompt.js +0 -1024
- package/esm/question/index.js +0 -1
- package/esm/resolvers/date.js +0 -11
- package/esm/resolvers/git.js +0 -26
- package/esm/resolvers/index.js +0 -103
- package/esm/resolvers/npm.js +0 -24
- package/esm/resolvers/workspace.js +0 -141
- package/esm/utils.js +0 -12
- package/keypress.d.ts +0 -45
- package/keypress.js +0 -99
- package/prompt.d.ts +0 -116
- package/prompt.js +0 -1032
- package/question/index.d.ts +0 -1
- package/question/types.d.ts +0 -65
- package/resolvers/date.d.ts +0 -5
- package/resolvers/date.js +0 -14
- package/resolvers/git.d.ts +0 -11
- package/resolvers/git.js +0 -30
- package/resolvers/index.d.ts +0 -63
- package/resolvers/index.js +0 -111
- package/resolvers/npm.d.ts +0 -10
- package/resolvers/npm.js +0 -28
- package/resolvers/types.d.ts +0 -12
- package/resolvers/workspace.d.ts +0 -6
- package/resolvers/workspace.js +0 -144
- package/utils.d.ts +0 -2
- package/utils.js +0 -16
- /package/{question → cache}/types.js +0 -0
- /package/esm/{question → cache}/types.js +0 -0
- /package/esm/{resolvers → git}/types.js +0 -0
- /package/{resolvers → git}/types.js +0 -0
package/README.md
CHANGED
|
@@ -11,13 +11,19 @@
|
|
|
11
11
|
<a href="https://github.com/constructive-io/dev-utils/blob/main/LICENSE">
|
|
12
12
|
<img height="20" src="https://img.shields.io/badge/license-MIT-blue.svg"/>
|
|
13
13
|
</a>
|
|
14
|
-
<a href="https://www.npmjs.com/package/genomic"><img height="20" src="https://img.shields.io/
|
|
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>
|
|
14
|
+
<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%2Fscaffolds%2Fpackage.json"></a>
|
|
16
15
|
</p>
|
|
17
16
|
|
|
18
|
-
|
|
17
|
+
A TypeScript-first library for cloning template repositories, asking the user for variables, and generating a new project with sensible defaults.
|
|
19
18
|
|
|
20
|
-
|
|
19
|
+
## Features
|
|
20
|
+
|
|
21
|
+
- Clone any Git repo (or GitHub `org/repo` shorthand) and optionally select a branch + subdirectory
|
|
22
|
+
- Extract template variables from filenames and file contents using the safer `____variable____` convention
|
|
23
|
+
- Merge auto-discovered variables with `.questions.{json,js}` (questions win)
|
|
24
|
+
- Interactive prompts powered by `genomic`, with flexible override mapping (`argv` support) and non-TTY mode for CI
|
|
25
|
+
- License scaffolding: choose from MIT, Apache-2.0, ISC, GPL-3.0, BSD-3-Clause, Unlicense, or MPL-2.0 and generate a populated `LICENSE`
|
|
26
|
+
- Built-in template caching powered by `appstash`, so repeat runs skip `git clone` (configurable via `cache` options; TTL is opt-in)
|
|
21
27
|
|
|
22
28
|
## Installation
|
|
23
29
|
|
|
@@ -25,1190 +31,210 @@ A powerful, TypeScript-first library for building beautiful command-line interfa
|
|
|
25
31
|
npm install genomic
|
|
26
32
|
```
|
|
27
33
|
|
|
28
|
-
|
|
34
|
+
> **Note:** The published package is API-only. An internal CLI harness used for integration testing now lives in `packages/genomic-test/`.
|
|
29
35
|
|
|
30
|
-
|
|
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
|
|
36
|
+
## Library Usage
|
|
37
37
|
|
|
38
|
-
|
|
38
|
+
`genomic` provides both a high-level orchestrator and modular building blocks for template scaffolding.
|
|
39
39
|
|
|
40
|
-
|
|
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)
|
|
40
|
+
### Quick Start with TemplateScaffolder
|
|
68
41
|
|
|
69
|
-
|
|
42
|
+
The easiest way to use `genomic` is with the `TemplateScaffolder` class, which combines caching, cloning, and template processing into a single API:
|
|
70
43
|
|
|
71
44
|
```typescript
|
|
72
|
-
import {
|
|
45
|
+
import { TemplateScaffolder } from 'genomic';
|
|
73
46
|
|
|
74
|
-
const
|
|
47
|
+
const scaffolder = new TemplateScaffolder({
|
|
48
|
+
toolName: 'my-cli', // Cache directory: ~/.my-cli/cache
|
|
49
|
+
defaultRepo: 'org/my-templates', // Default template repository
|
|
50
|
+
ttlMs: 7 * 24 * 60 * 60 * 1000, // Cache TTL: 1 week
|
|
51
|
+
});
|
|
75
52
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
},
|
|
83
|
-
{
|
|
84
|
-
type: 'confirm',
|
|
85
|
-
name: 'newsletter',
|
|
86
|
-
message: 'Subscribe to our newsletter?',
|
|
87
|
-
default: true
|
|
88
|
-
}
|
|
89
|
-
]);
|
|
53
|
+
// Scaffold a project from the default repo
|
|
54
|
+
await scaffolder.scaffold({
|
|
55
|
+
outputDir: './my-project',
|
|
56
|
+
fromPath: 'starter', // Use the "starter" template variant
|
|
57
|
+
answers: { projectName: 'my-app' }, // Pre-populate answers
|
|
58
|
+
});
|
|
90
59
|
|
|
91
|
-
|
|
92
|
-
|
|
60
|
+
// Or scaffold from a specific repo
|
|
61
|
+
await scaffolder.scaffold({
|
|
62
|
+
template: 'https://github.com/other/templates.git',
|
|
63
|
+
outputDir: './another-project',
|
|
64
|
+
branch: 'v2',
|
|
65
|
+
});
|
|
93
66
|
```
|
|
94
67
|
|
|
95
|
-
|
|
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';
|
|
68
|
+
### Template Repository Conventions
|
|
116
69
|
|
|
117
|
-
|
|
118
|
-
name: string;
|
|
119
|
-
age: number;
|
|
120
|
-
newsletter: boolean;
|
|
121
|
-
}
|
|
70
|
+
`TemplateScaffolder` supports the `.boilerplates.json` convention for organizing multiple templates in a single repository:
|
|
122
71
|
|
|
123
|
-
const answers = await prompter.prompt<UserConfig>({}, questions);
|
|
124
|
-
// answers is typed as UserConfig
|
|
125
72
|
```
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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
|
-
}
|
|
73
|
+
my-templates/
|
|
74
|
+
├── .boilerplates.json # { "dir": "templates" }
|
|
75
|
+
└── templates/
|
|
76
|
+
├── starter/
|
|
77
|
+
│ ├── .boilerplate.json
|
|
78
|
+
│ └── ...template files...
|
|
79
|
+
└── advanced/
|
|
80
|
+
├── .boilerplate.json
|
|
81
|
+
└── ...template files...
|
|
149
82
|
```
|
|
150
83
|
|
|
151
|
-
|
|
84
|
+
When you call `scaffold({ fromPath: 'starter' })`, the scaffolder will:
|
|
85
|
+
1. Check if `starter/` exists directly in the repo root
|
|
86
|
+
2. If not, read `.boilerplates.json` and look for `templates/starter/`
|
|
152
87
|
|
|
153
|
-
|
|
88
|
+
### Core Components (Building Blocks)
|
|
154
89
|
|
|
155
|
-
|
|
156
|
-
const prompter = new Prompter({
|
|
157
|
-
noTty: true, // Force non-interactive mode
|
|
158
|
-
useDefaults: true // Use defaults without prompting
|
|
159
|
-
});
|
|
160
|
-
```
|
|
90
|
+
For more control, you can use the individual components directly:
|
|
161
91
|
|
|
162
|
-
|
|
92
|
+
- **CacheManager**: Handles local caching of git repositories with TTL support
|
|
93
|
+
- **GitCloner**: Handles cloning git repositories
|
|
94
|
+
- **Templatizer**: Handles variable extraction, user prompting, and template generation
|
|
163
95
|
|
|
164
|
-
###
|
|
96
|
+
### Example: Manual Orchestration
|
|
165
97
|
|
|
166
|
-
|
|
98
|
+
Here is how you can combine these components to create a full CLI pipeline (similar to `genomic-test`):
|
|
167
99
|
|
|
168
100
|
```typescript
|
|
169
|
-
|
|
170
|
-
|
|
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
|
|
101
|
+
import * as path from "path";
|
|
102
|
+
import { CacheManager, GitCloner, Templatizer } from "genomic";
|
|
197
103
|
|
|
198
|
-
|
|
104
|
+
async function main() {
|
|
105
|
+
const repoUrl = "https://github.com/user/template-repo";
|
|
106
|
+
const outputDir = "./my-new-project";
|
|
199
107
|
|
|
200
|
-
|
|
108
|
+
// 1. Initialize components
|
|
109
|
+
const cacheManager = new CacheManager({
|
|
110
|
+
toolName: "my-cli", // ~/.my-cli/cache
|
|
111
|
+
// ttl is optional; omit to keep cache forever, or set (e.g., 1 week) to enable expiration
|
|
112
|
+
// ttl: 604800000,
|
|
113
|
+
});
|
|
114
|
+
const gitCloner = new GitCloner();
|
|
115
|
+
const templatizer = new Templatizer();
|
|
201
116
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
prompter.close(); // Clean up when done
|
|
211
|
-
```
|
|
117
|
+
// 2. Resolve template path (Cache or Clone)
|
|
118
|
+
const normalizedUrl = gitCloner.normalizeUrl(repoUrl);
|
|
119
|
+
const cacheKey = cacheManager.createKey(normalizedUrl);
|
|
120
|
+
|
|
121
|
+
// Check cache
|
|
122
|
+
let templateDir = cacheManager.get(cacheKey);
|
|
123
|
+
const isExpired = cacheManager.checkExpiration(cacheKey);
|
|
212
124
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
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;
|
|
125
|
+
if (!templateDir || isExpired) {
|
|
126
|
+
console.log("Cloning template...");
|
|
127
|
+
if (isExpired) cacheManager.clear(cacheKey);
|
|
128
|
+
|
|
129
|
+
// Clone to a temporary location managed by CacheManager
|
|
130
|
+
const tempDest = path.join(cacheManager.getReposDir(), cacheKey);
|
|
131
|
+
await gitCloner.clone(normalizedUrl, tempDest, { depth: 1 });
|
|
132
|
+
|
|
133
|
+
// Register and update cache
|
|
134
|
+
cacheManager.set(cacheKey, tempDest);
|
|
135
|
+
templateDir = tempDest;
|
|
243
136
|
}
|
|
244
|
-
}
|
|
245
|
-
```
|
|
246
137
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
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' };
|
|
138
|
+
// 3. Process Template
|
|
139
|
+
await templatizer.process(templateDir, outputDir, {
|
|
140
|
+
argv: {
|
|
141
|
+
PROJECT_NAME: "my-app",
|
|
142
|
+
LICENSE: "MIT"
|
|
260
143
|
}
|
|
261
|
-
|
|
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
|
|
144
|
+
});
|
|
291
145
|
}
|
|
292
146
|
```
|
|
293
147
|
|
|
294
|
-
|
|
148
|
+
### Template Variables
|
|
295
149
|
|
|
296
|
-
|
|
150
|
+
Variables should be wrapped in four underscores on each side:
|
|
297
151
|
|
|
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
152
|
```
|
|
313
|
-
|
|
314
|
-
|
|
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
|
-
}
|
|
153
|
+
____projectName____/
|
|
154
|
+
src/____moduleName____.ts
|
|
334
155
|
```
|
|
335
156
|
|
|
336
|
-
With `returnFullResults: true`, returns all options with selection status:
|
|
337
|
-
|
|
338
157
|
```typescript
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
// ...
|
|
343
|
-
]
|
|
158
|
+
// ____moduleName____.ts
|
|
159
|
+
export const projectName = "____projectName____";
|
|
160
|
+
export const author = "____fullName____";
|
|
344
161
|
```
|
|
345
162
|
|
|
346
|
-
###
|
|
163
|
+
### Custom Questions
|
|
347
164
|
|
|
348
|
-
|
|
165
|
+
Create a `.boilerplate.json`:
|
|
349
166
|
|
|
350
|
-
```
|
|
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
|
|
167
|
+
```json
|
|
372
168
|
{
|
|
373
|
-
type:
|
|
374
|
-
|
|
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[] = [
|
|
169
|
+
"type": "module",
|
|
170
|
+
"questions": [
|
|
646
171
|
{
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
message:
|
|
650
|
-
required: true
|
|
172
|
+
"name": "____fullName____",
|
|
173
|
+
"type": "text",
|
|
174
|
+
"message": "Enter author full name",
|
|
175
|
+
"required": true
|
|
651
176
|
},
|
|
652
177
|
{
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
message:
|
|
656
|
-
|
|
178
|
+
"name": "____license____",
|
|
179
|
+
"type": "list",
|
|
180
|
+
"message": "Choose a license",
|
|
181
|
+
"options": ["MIT", "Apache-2.0", "ISC", "GPL-3.0"]
|
|
657
182
|
}
|
|
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
|
|
183
|
+
]
|
|
1081
184
|
}
|
|
1082
185
|
```
|
|
1083
186
|
|
|
1084
|
-
|
|
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!
|
|
187
|
+
Or `.boilerplate.js` for dynamic logic. Question names can use `____var____` or plain `VAR`; they'll be normalized automatically.
|
|
1144
188
|
|
|
1145
|
-
|
|
189
|
+
Note: `.boilerplate.json`, `.boilerplate.js`, and `.boilerplates.json` files are automatically excluded from the generated output.
|
|
1146
190
|
|
|
1147
|
-
|
|
191
|
+
### License Templates
|
|
1148
192
|
|
|
1149
|
-
|
|
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
|
-
};
|
|
193
|
+
`genomic` ships text templates in `licenses-templates/`. To add another license, drop a `.txt` file matching the desired key (e.g., `BSD-2-CLAUSE.txt`) with placeholders:
|
|
1173
194
|
|
|
1174
|
-
|
|
1175
|
-
await cli.run();
|
|
1176
|
-
```
|
|
195
|
+
- `{{YEAR}}`, `{{AUTHOR}}`, `{{EMAIL_LINE}}`
|
|
1177
196
|
|
|
1178
|
-
|
|
197
|
+
No code changes are needed; the generator discovers templates at runtime and will warn if a `.questions` option doesn’t have a matching template.
|
|
1179
198
|
|
|
1180
|
-
##
|
|
199
|
+
## API Overview
|
|
1181
200
|
|
|
1182
|
-
###
|
|
201
|
+
### TemplateScaffolder (Recommended)
|
|
1183
202
|
|
|
1184
|
-
|
|
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
|
-
```
|
|
203
|
+
The high-level orchestrator that combines caching, cloning, and template processing:
|
|
1204
204
|
|
|
1205
|
-
|
|
205
|
+
- `new TemplateScaffolder(config)`: Initialize with configuration:
|
|
206
|
+
- `toolName` (required): Name for cache directory (e.g., `'my-cli'` → `~/.my-cli/cache`)
|
|
207
|
+
- `defaultRepo`: Default template repository URL or `org/repo` shorthand
|
|
208
|
+
- `defaultBranch`: Default branch to clone
|
|
209
|
+
- `ttlMs`: Cache time-to-live in milliseconds
|
|
210
|
+
- `cacheBaseDir`: Override cache location (useful for tests)
|
|
211
|
+
- `scaffold(options)`: Scaffold a project from a template:
|
|
212
|
+
- `template`: Repository URL, local path, or `org/repo` shorthand (uses `defaultRepo` if not provided)
|
|
213
|
+
- `outputDir` (required): Output directory for generated project
|
|
214
|
+
- `fromPath`: Subdirectory within template to use
|
|
215
|
+
- `branch`: Branch to clone
|
|
216
|
+
- `answers`: Pre-populated answers to skip prompting
|
|
217
|
+
- `noTty`: Disable interactive prompts
|
|
218
|
+
- `prompter`: Reuse an existing Genomic instance
|
|
219
|
+
- `readBoilerplatesConfig(dir)`: Read `.boilerplates.json` from a template repo
|
|
220
|
+
- `readBoilerplateConfig(dir)`: Read `.boilerplate.json` from a template directory
|
|
221
|
+
- `getCacheManager()`, `getGitCloner()`, `getTemplatizer()`: Access underlying components
|
|
1206
222
|
|
|
1207
|
-
|
|
1208
|
-
|
|
223
|
+
### CacheManager
|
|
224
|
+
- `new CacheManager(config)`: Initialize with `toolName` and optional `ttl`.
|
|
225
|
+
- `get(key)`: Get path to cached repo if exists.
|
|
226
|
+
- `set(key, path)`: Register a path in the cache.
|
|
227
|
+
- `checkExpiration(key)`: Check if a cache entry is expired.
|
|
228
|
+
- `clear(key)`: Remove a specific cache entry.
|
|
229
|
+
- `clearAll()`: Clear all cached repos.
|
|
230
|
+
- When `ttl` is `undefined`, cache entries never expire. Provide a TTL (ms) only when you want automatic invalidation.
|
|
231
|
+
- Advanced: if you already own an appstash instance, pass `dirs` to reuse it instead of letting CacheManager create its own.
|
|
1209
232
|
|
|
1210
|
-
|
|
233
|
+
### GitCloner
|
|
234
|
+
- `clone(url, dest, options)`: Clone a repo to a destination.
|
|
235
|
+
- `normalizeUrl(url)`: Normalize a git URL for consistency.
|
|
1211
236
|
|
|
1212
|
-
|
|
237
|
+
### Templatizer
|
|
238
|
+
- `process(templateDir, outputDir, options)`: Run the full template generation pipeline (extract -> prompt -> replace).
|
|
1213
239
|
|
|
1214
|
-
|
|
240
|
+
See `packages/genomic-test` for a complete reference implementation.
|