genomic 4.0.2 → 5.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. package/README.md +154 -1125
  2. package/cache/cache-manager.d.ts +60 -0
  3. package/cache/cache-manager.js +228 -0
  4. package/cache/types.d.ts +22 -0
  5. package/esm/cache/cache-manager.js +191 -0
  6. package/esm/git/git-cloner.js +92 -0
  7. package/esm/index.js +41 -4
  8. package/esm/licenses.js +120 -0
  9. package/esm/scaffolder/index.js +2 -0
  10. package/esm/scaffolder/template-scaffolder.js +310 -0
  11. package/esm/scaffolder/types.js +1 -0
  12. package/esm/template/extract.js +162 -0
  13. package/esm/template/prompt.js +103 -0
  14. package/esm/template/replace.js +110 -0
  15. package/esm/template/templatizer.js +73 -0
  16. package/esm/template/types.js +1 -0
  17. package/esm/types.js +1 -0
  18. package/esm/utils/npm-version-check.js +52 -0
  19. package/esm/utils/types.js +1 -0
  20. package/git/git-cloner.d.ts +32 -0
  21. package/git/git-cloner.js +129 -0
  22. package/git/types.d.ts +15 -0
  23. package/index.d.ts +29 -4
  24. package/index.js +43 -4
  25. package/licenses-templates/APACHE-2.0.txt +18 -0
  26. package/licenses-templates/BSD-3-CLAUSE.txt +28 -0
  27. package/licenses-templates/CLOSED.txt +20 -0
  28. package/licenses-templates/GPL-3.0.txt +18 -0
  29. package/licenses-templates/ISC.txt +16 -0
  30. package/licenses-templates/MIT.txt +22 -0
  31. package/licenses-templates/MPL-2.0.txt +8 -0
  32. package/licenses-templates/UNLICENSE.txt +22 -0
  33. package/licenses.d.ts +18 -0
  34. package/licenses.js +162 -0
  35. package/package.json +9 -14
  36. package/scaffolder/index.d.ts +2 -0
  37. package/{question → scaffolder}/index.js +1 -0
  38. package/scaffolder/template-scaffolder.d.ts +91 -0
  39. package/scaffolder/template-scaffolder.js +347 -0
  40. package/scaffolder/types.d.ts +191 -0
  41. package/scaffolder/types.js +2 -0
  42. package/template/extract.d.ts +7 -0
  43. package/template/extract.js +198 -0
  44. package/template/prompt.d.ts +19 -0
  45. package/template/prompt.js +107 -0
  46. package/template/replace.d.ts +9 -0
  47. package/template/replace.js +146 -0
  48. package/template/templatizer.d.ts +33 -0
  49. package/template/templatizer.js +110 -0
  50. package/template/types.d.ts +18 -0
  51. package/template/types.js +2 -0
  52. package/types.d.ts +99 -0
  53. package/types.js +2 -0
  54. package/utils/npm-version-check.d.ts +17 -0
  55. package/utils/npm-version-check.js +57 -0
  56. package/utils/types.d.ts +6 -0
  57. package/utils/types.js +2 -0
  58. package/commander.d.ts +0 -21
  59. package/commander.js +0 -57
  60. package/esm/commander.js +0 -50
  61. package/esm/keypress.js +0 -95
  62. package/esm/prompt.js +0 -1024
  63. package/esm/question/index.js +0 -1
  64. package/esm/resolvers/date.js +0 -11
  65. package/esm/resolvers/git.js +0 -26
  66. package/esm/resolvers/index.js +0 -103
  67. package/esm/resolvers/npm.js +0 -24
  68. package/esm/resolvers/workspace.js +0 -141
  69. package/esm/utils.js +0 -12
  70. package/keypress.d.ts +0 -45
  71. package/keypress.js +0 -99
  72. package/prompt.d.ts +0 -116
  73. package/prompt.js +0 -1032
  74. package/question/index.d.ts +0 -1
  75. package/question/types.d.ts +0 -65
  76. package/resolvers/date.d.ts +0 -5
  77. package/resolvers/date.js +0 -14
  78. package/resolvers/git.d.ts +0 -11
  79. package/resolvers/git.js +0 -30
  80. package/resolvers/index.d.ts +0 -63
  81. package/resolvers/index.js +0 -111
  82. package/resolvers/npm.d.ts +0 -10
  83. package/resolvers/npm.js +0 -28
  84. package/resolvers/types.d.ts +0 -12
  85. package/resolvers/workspace.d.ts +0 -6
  86. package/resolvers/workspace.js +0 -144
  87. package/utils.d.ts +0 -2
  88. package/utils.js +0 -16
  89. /package/{question → cache}/types.js +0 -0
  90. /package/esm/{question → cache}/types.js +0 -0
  91. /package/esm/{resolvers → git}/types.js +0 -0
  92. /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%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>
15
15
  </p>
16
16
 
17
- A powerful, TypeScript-first library for building beautiful command-line interfaces.Create interactive CLI tools with ease using intuitive prompts, validation, and rich user experiences.
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
- ## Features
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
- - 🔌 **CLI Builder** - Build command-line utilties fast
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
- ## Table of Contents
38
+ `genomic` provides both a high-level orchestrator and modular building blocks for template scaffolding.
36
39
 
37
- - [Quick Start](#quick-start)
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
- ## Quick Start
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 { Prompter } from 'genomic';
45
+ import { TemplateScaffolder } from 'genomic';
70
46
 
71
- const prompter = new Prompter();
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
- const answers = await prompter.prompt({}, [
74
- {
75
- type: 'text',
76
- name: 'username',
77
- message: 'What is your username?',
78
- required: true
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
- console.log(answers);
89
- // { username: 'john_doe', newsletter: true }
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
- ## Core Concepts
93
-
94
- ### TypeScript Support
95
-
96
- Import types for full type safety:
68
+ ### Template Repository Conventions
97
69
 
98
- ```typescript
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
- ### Question Types
125
-
126
- All questions support these base properties:
127
-
128
- ```typescript
129
- interface BaseQuestion {
130
- name: string; // Property name in result object
131
- type: string; // Question type
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
- ### Non-Interactive Mode
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
- When running in CI/CD or without a TTY, genomic automatically falls back to default values:
88
+ ### Core Components (Building Blocks)
151
89
 
152
- ```typescript
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
- #### Constructor Options
164
-
165
- ```typescript
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
- const prompter = new Prompter(options);
177
- ```
96
+ ### Example: Manual Orchestration
178
97
 
179
- #### Methods
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
- // Main prompt method
183
- prompt<T>(argv: T, questions: Question[], options?: PromptOptions): Promise<T>
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
- #### Managing Multiple Instances
104
+ async function main() {
105
+ const repoUrl = "https://github.com/user/template-repo";
106
+ const outputDir = "./my-new-project";
194
107
 
195
- When working with multiple `Prompter` instances that share the same input stream (typically `process.stdin`), only one instance should be actively prompting at a time. Each instance attaches its own keyboard listener, so having multiple active instances will cause duplicate or unexpected keypress behavior.
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
- **Best practices:**
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
- 1. **Reuse a single instance** - Create one `Prompter` instance and reuse it for all prompts:
200
- ```typescript
201
- const prompter = new Prompter();
202
-
203
- // Use the same instance for multiple prompt sessions
204
- const answers1 = await prompter.prompt({}, questions1);
205
- const answers2 = await prompter.prompt({}, questions2);
206
-
207
- prompter.close(); // Clean up when done
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
- ```typescript
249
- {
250
- type: 'number',
251
- name: 'port',
252
- message: 'Which port to use?',
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
- return true;
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
- #### Autocomplete Question
148
+ ### Template Variables
292
149
 
293
- Select with fuzzy search capabilities.
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
- #### Checkbox Question
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
- { name: 'Authentication', value: 'Authentication', selected: true },
338
- { name: 'Database', value: 'Database', selected: false },
339
- // ...
340
- ]
158
+ // ____moduleName____.ts
159
+ export const projectName = "____projectName____";
160
+ export const author = "____fullName____";
341
161
  ```
342
162
 
343
- ### Advanced Question Options
163
+ ### Custom Questions
344
164
 
345
- #### Custom Validation
165
+ Create a `.boilerplate.json`:
346
166
 
347
- ```typescript
167
+ ```json
348
168
  {
349
- type: 'text',
350
- name: 'email',
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
- type: 'text',
645
- name: 'name',
646
- message: 'What is your name?',
647
- required: true
172
+ "name": "____fullName____",
173
+ "type": "text",
174
+ "message": "Enter author full name",
175
+ "required": true
648
176
  },
649
177
  {
650
- type: 'number',
651
- name: 'age',
652
- message: 'What is your age?',
653
- validate: (age) => age >= 0 && age <= 120
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
- #### When to use each
187
+ Or `.boilerplate.js` for dynamic logic. Question names can use `____var____` or plain `VAR`; they'll be normalized automatically.
925
188
 
926
- Use `defaultFrom` when:
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
- Use `setFrom` when:
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
- #### Combined example
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
- ### Custom Resolvers
195
+ - `{{YEAR}}`, `{{AUTHOR}}`, `{{EMAIL_LINE}}`
962
196
 
963
- Register your own custom resolvers for project-specific needs:
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
- ### Instance-Specific Resolvers
199
+ ## API Overview
997
200
 
998
- For isolated resolver registries, use a custom resolver registry per Prompter instance:
201
+ ### TemplateScaffolder (Recommended)
999
202
 
1000
- ```typescript
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
- ## Credits
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
- Built for developers, with developers.
1205
- 👉 https://launchql.com | https://hyperweb.io
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
- ## Disclaimer
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
- AS DESCRIBED IN THE LICENSES, THE SOFTWARE IS PROVIDED "AS IS", AT YOUR OWN RISK, AND WITHOUT WARRANTIES OF ANY KIND.
237
+ ### Templatizer
238
+ - `process(templateDir, outputDir, options)`: Run the full template generation pipeline (extract -> prompt -> replace).
1210
239
 
1211
- No developer or entity involved in creating this software will be liable for any claims or damages whatsoever associated with your use, inability to use, or your interaction with other users of the code, including any direct, indirect, incidental, special, exemplary, punitive or consequential damages, or loss of profits, cryptocurrencies, tokens, or anything else of value.
240
+ See `packages/genomic-test` for a complete reference implementation.