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