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.
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/prompt.js DELETED
@@ -1,1032 +0,0 @@
1
- "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
- Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.Prompter = void 0;
7
- exports.reorderQuestionsByDeps = reorderQuestionsByDeps;
8
- const yanse_1 = require("yanse");
9
- const readline_1 = __importDefault(require("readline"));
10
- const keypress_1 = require("./keypress");
11
- const resolvers_1 = require("./resolvers");
12
- function reorderQuestionsByDeps(questions) {
13
- const nameToIndex = new Map();
14
- questions.forEach((q, idx) => nameToIndex.set(q.name, idx));
15
- const resolved = new Set();
16
- const result = [];
17
- function addQuestion(q) {
18
- // If this question depends on others, ensure those are added first
19
- if (q.dependsOn && q.dependsOn.length > 0) {
20
- for (const dep of q.dependsOn) {
21
- if (!resolved.has(dep)) {
22
- const depIdx = nameToIndex.get(dep);
23
- if (depIdx === undefined) {
24
- throw new Error(`Unknown dependency: ${dep}`);
25
- }
26
- addQuestion(questions[depIdx]);
27
- }
28
- }
29
- }
30
- if (!resolved.has(q.name)) {
31
- resolved.add(q.name);
32
- result.push(q);
33
- }
34
- }
35
- for (const q of questions) {
36
- addQuestion(q);
37
- }
38
- return result;
39
- }
40
- const validationMessage = (question, ctx) => {
41
- if (ctx.numTries === 0 || ctx.validation.success) {
42
- return ''; // No message if first attempt or validation was successful
43
- }
44
- if (ctx.validation.reason) {
45
- return (0, yanse_1.red)(`The field "${question.name}" is invalid: ${ctx.validation.reason}\n`);
46
- }
47
- switch (ctx.validation.type) {
48
- case 'required':
49
- return (0, yanse_1.red)(`The field "${question.name}" is required. Please provide a value.\n`);
50
- case 'pattern':
51
- return (0, yanse_1.red)(`The field "${question.name}" does not match the pattern: ${question.pattern}.\n`);
52
- default:
53
- return (0, yanse_1.red)(`The field "${question.name}" is invalid. Please try again.\n`);
54
- }
55
- };
56
- class PromptContext {
57
- numTries = 0;
58
- needsInput = true;
59
- validation = { success: false };
60
- constructor() { }
61
- tryAgain(validation) {
62
- this.numTries++;
63
- this.needsInput = true;
64
- this.validation = { ...this.validation, ...validation, success: false };
65
- }
66
- nextQuestion() {
67
- this.numTries = 0;
68
- this.needsInput = false;
69
- this.validation = { success: true };
70
- }
71
- process(validation) {
72
- if (typeof validation === 'boolean') {
73
- if (validation) {
74
- this.nextQuestion();
75
- }
76
- else {
77
- this.tryAgain({ type: 'validation' });
78
- }
79
- }
80
- else {
81
- if (validation.success) {
82
- this.nextQuestion();
83
- }
84
- else {
85
- this.tryAgain(validation);
86
- }
87
- }
88
- return this.validation;
89
- }
90
- }
91
- function generatePromptMessage(question, ctx) {
92
- const { message, name, type, default: def, options = [], description } = question;
93
- const lines = [];
94
- // 1. Main prompt label with --name inline
95
- let promptLine = yanse_1.whiteBright.bold(message || `${name}?`) + ' ' + (0, yanse_1.dim)(`(--${name})`);
96
- // 2. Append default inline (only if present)
97
- switch (type) {
98
- case 'confirm':
99
- promptLine += ' (y/n)';
100
- if (def !== undefined) {
101
- promptLine += ` ${(0, yanse_1.yellow)(`[${def ? 'y' : 'n'}]`)}`;
102
- }
103
- break;
104
- case 'text':
105
- case 'number':
106
- if (def !== undefined) {
107
- promptLine += ` ${(0, yanse_1.yellow)(`[${def}]`)}`;
108
- }
109
- break;
110
- case 'autocomplete':
111
- case 'list':
112
- case 'checkbox':
113
- if (def !== undefined) {
114
- const defaults = Array.isArray(def) ? def : [def];
115
- const rendered = defaults.map(d => (0, yanse_1.yellow)(d)).join((0, yanse_1.gray)(', '));
116
- promptLine += ` ${(0, yanse_1.yellow)(`[${rendered}]`)}`;
117
- }
118
- break;
119
- }
120
- lines.push(promptLine);
121
- // 3. Optional description below title
122
- if (description) {
123
- lines.push((0, yanse_1.dim)(description));
124
- }
125
- // 4. Validation message if applicable
126
- const validation = validationMessage(question, ctx);
127
- if (validation) {
128
- lines.push(validation); // already styled red
129
- }
130
- return lines.join('\n') + '\n';
131
- }
132
- class Prompter {
133
- rl;
134
- keypress;
135
- noTty;
136
- output;
137
- input;
138
- useDefaults;
139
- globalMaxLines;
140
- mutateArgs;
141
- resolverRegistry;
142
- handledKeys = new Set();
143
- constructor(options) {
144
- const { noTty = false, input = process.stdin, output = process.stdout, useDefaults = false, globalMaxLines = 10, mutateArgs = true, resolverRegistry = resolvers_1.globalResolverRegistry } = options ?? {};
145
- this.useDefaults = useDefaults;
146
- this.noTty = noTty;
147
- this.output = output;
148
- this.mutateArgs = mutateArgs;
149
- this.input = input;
150
- this.globalMaxLines = globalMaxLines;
151
- this.resolverRegistry = resolverRegistry;
152
- if (!noTty) {
153
- this.rl = readline_1.default.createInterface({
154
- input,
155
- output
156
- });
157
- this.keypress = new keypress_1.TerminalKeypress(noTty, input);
158
- }
159
- else {
160
- this.rl = null;
161
- this.keypress = null;
162
- }
163
- }
164
- clearScreen() {
165
- // same as console.clear()
166
- this.output.write('\x1Bc'); // This is the escape sequence to clear the terminal screen.
167
- }
168
- write(message) {
169
- this.output.write(message);
170
- }
171
- log(message) {
172
- this.output.write(message + '\n');
173
- }
174
- getInput(input) {
175
- return `${(0, yanse_1.white)('>')} ${input}`;
176
- }
177
- getPrompt(question, ctx, input) {
178
- const promptMessage = generatePromptMessage(question, ctx);
179
- return promptMessage + this.getInput(input);
180
- }
181
- displayPrompt(question, ctx, input) {
182
- const prompt = this.getPrompt(question, ctx, input);
183
- this.log(prompt);
184
- }
185
- generateManPage(opts) {
186
- let manPage = `${(0, yanse_1.white)('NAME')}\n\t${(0, yanse_1.white)(opts.commandName)} ${opts.description ?? ''}\n\n`;
187
- // Constructing the SYNOPSIS section with required and optional arguments
188
- let requiredArgs = '';
189
- let optionalArgs = '';
190
- opts.questions.forEach(question => {
191
- if (question.required) {
192
- requiredArgs += ` ${(0, yanse_1.white)('--' + question.name)} <${(0, yanse_1.gray)(question.name)}>`;
193
- }
194
- else {
195
- optionalArgs += ` [${(0, yanse_1.white)('--' + question.name)}${question.default ? `=${(0, yanse_1.gray)(String(question.default))}` : ''}]`;
196
- }
197
- });
198
- manPage += `${(0, yanse_1.white)('SYNOPSIS')}\n\t${(0, yanse_1.white)(opts.commandName)}${(0, yanse_1.gray)(requiredArgs)}${(0, yanse_1.gray)(optionalArgs)}\n\n`;
199
- manPage += `${(0, yanse_1.white)('DESCRIPTION')}\n\tUse this command to interact with the application. It supports the following options:\n\n`;
200
- opts.questions.forEach(question => {
201
- manPage += `${(0, yanse_1.white)(question.name.toUpperCase())}\n`;
202
- manPage += `\t${(0, yanse_1.white)('Type:')} ${(0, yanse_1.gray)(question.type)}\n`;
203
- if (question.message) {
204
- manPage += `\t${(0, yanse_1.white)('Summary:')} ${(0, yanse_1.gray)(question.message)}\n`;
205
- }
206
- if (question.description) {
207
- manPage += `\t${(0, yanse_1.white)('Description:')} ${(0, yanse_1.gray)(question.description)}\n`;
208
- }
209
- if ('options' in question) {
210
- const optionsList = Array.isArray(question.options)
211
- ? question.options.map(opt => typeof opt === 'string' ? (0, yanse_1.gray)(opt) : `${(0, yanse_1.gray)(opt.name)} (${(0, yanse_1.gray)(opt.value)})`).join(', ')
212
- : '';
213
- manPage += `\t${(0, yanse_1.white)('Options:')} ${(0, yanse_1.gray)(optionsList)}\n`;
214
- }
215
- if (question.default !== undefined) {
216
- manPage += `\t${(0, yanse_1.white)('Default:')} ${(0, yanse_1.gray)(JSON.stringify(question.default))}\n`;
217
- }
218
- if (question.required) {
219
- manPage += `\t${(0, yanse_1.white)('Required:')} ${(0, yanse_1.gray)('Yes')}\n`;
220
- }
221
- else {
222
- manPage += `\t${(0, yanse_1.white)('Required:')} ${(0, yanse_1.gray)('No')}\n`;
223
- }
224
- manPage += '\n';
225
- });
226
- manPage += `${(0, yanse_1.white)('EXAMPLES')}\n\tExample usage of \`${(0, yanse_1.white)(opts.commandName)}\`.\n\t$ ${(0, yanse_1.white)(opts.commandName)}${(0, yanse_1.gray)(requiredArgs)}${(0, yanse_1.gray)(optionalArgs)}\n\n`;
227
- manPage += opts.author ? `${(0, yanse_1.white)('AUTHOR')}\n\t${(0, yanse_1.white)(opts.author)}\n` : '';
228
- return manPage;
229
- }
230
- isValidatableAnswer(answer) {
231
- return answer !== undefined;
232
- }
233
- validateAnswer(question, answer, obj, ctx) {
234
- const validation = this.validateAnswerPattern(question, answer);
235
- if (!validation.success) {
236
- return ctx.process(validation);
237
- }
238
- if (question.validate) {
239
- const customValidation = question.validate(answer, obj);
240
- return ctx.process(customValidation);
241
- }
242
- return ctx.process({
243
- success: true
244
- });
245
- }
246
- isValid(question, obj, ctx) {
247
- if (this.isValidatableAnswer(obj[question.name])) {
248
- obj[question.name] = this.sanitizeAnswer(question, obj[question.name], obj);
249
- const validationResult = this.validateAnswer(question, obj[question.name], obj, ctx);
250
- if (!validationResult.success) {
251
- return false;
252
- }
253
- }
254
- if (question.required && this.isEmptyAnswer(obj[question.name])) {
255
- ctx.tryAgain({ type: 'required' });
256
- return false;
257
- }
258
- return true;
259
- }
260
- validateAnswerPattern(question, answer) {
261
- if (question.pattern && typeof answer === 'string') {
262
- const regex = new RegExp(question.pattern);
263
- const success = regex.test(answer);
264
- if (success) {
265
- return {
266
- success
267
- };
268
- }
269
- else {
270
- return {
271
- type: 'pattern',
272
- success: false,
273
- reason: question.pattern
274
- };
275
- }
276
- }
277
- return {
278
- success: true
279
- };
280
- }
281
- isEmptyAnswer(answer) {
282
- switch (true) {
283
- case answer === undefined:
284
- case answer === null:
285
- case answer === '':
286
- case Array.isArray(answer) && answer.length === 0:
287
- return true;
288
- }
289
- return false;
290
- }
291
- sanitizeAnswer(question, answer, obj) {
292
- if (question.sanitize) {
293
- return question.sanitize(answer, obj);
294
- }
295
- return answer;
296
- }
297
- exit() {
298
- this.clearScreen();
299
- this.close();
300
- }
301
- async prompt(argv, questions, options) {
302
- // use local mutateArgs if defined, otherwise global mutateArgs
303
- const shouldMutate = options?.mutateArgs !== undefined ? options.mutateArgs : this.mutateArgs;
304
- // Create a working copy of argv - deep clone the _ array to avoid shared reference
305
- let obj = shouldMutate ? argv : { ...argv };
306
- const argvAny = argv;
307
- if (!shouldMutate && Array.isArray(argvAny._)) {
308
- obj._ = [...argvAny._];
309
- }
310
- // Resolve dynamic defaults before processing questions
311
- await this.resolveDynamicDefaults(questions);
312
- // Resolve dynamic options before processing questions
313
- await this.resolveOptionsFrom(questions);
314
- // Resolve setFrom values - these bypass prompting entirely
315
- await this.resolveSetValues(questions, obj);
316
- // Extract positional arguments from argv._ and assign to questions with _: true
317
- // This must happen before applyOverrides so positional values flow through override pipeline
318
- // Returns the number of positional arguments consumed for stripping
319
- const consumedCount = this.extractPositionalArgs(obj, questions);
320
- // Strip consumed positionals from obj._ (the working copy)
321
- if (consumedCount > 0 && Array.isArray(obj._)) {
322
- obj._ = obj._.slice(consumedCount);
323
- // If mutating, also update the original argv._
324
- if (shouldMutate) {
325
- argvAny._ = obj._;
326
- }
327
- }
328
- // first loop through the question, and set any overrides in case other questions use objs for validation
329
- this.applyOverrides(obj, obj, questions);
330
- // Check for required arguments when no terminal is available (non-interactive mode)
331
- if (this.noTty && this.hasMissingRequiredArgs(questions, argv)) {
332
- // Apply default values for all questions
333
- this.applyDefaultValues(questions, obj);
334
- // Recheck for missing required arguments after applying defaults
335
- // NOT so sure this would ever happen, but possible if developer did something wrong
336
- if (!this.hasMissingRequiredArgs(questions, argv)) {
337
- return obj; // Return the updated object if no required arguments are missing
338
- }
339
- // Handle error for missing required arguments
340
- this.handleMissingArgsError(options);
341
- throw new Error('Missing required arguments. Please provide all required parameters.');
342
- }
343
- const ordered = reorderQuestionsByDeps(questions);
344
- for (let index = 0; index < ordered.length; index++) {
345
- const question = ordered[index];
346
- const ctx = new PromptContext();
347
- // obj is already either argv itself, or a clone, but let's check if it has the property
348
- if (question.name in obj) {
349
- this.handleOverrides(argv, obj, question);
350
- ctx.nextQuestion();
351
- continue;
352
- }
353
- if (question.when && !question.when(obj)) {
354
- ctx.nextQuestion();
355
- continue;
356
- }
357
- // Apply default value if applicable
358
- // this is if useDefault is set, rare! not typical defaults which happen AFTER
359
- // this is mostly to avoid a prompt for "hidden" options
360
- if ('default' in question && (this.useDefaults || question.useDefault)) {
361
- obj[question.name] = question.default;
362
- continue; // Skip to the next question since the default is applied
363
- }
364
- while (ctx.needsInput) {
365
- obj[question.name] = await this.handleQuestionType(question, ctx);
366
- if (!this.isValid(question, obj, ctx)) {
367
- if (this.noTty) {
368
- // If you're not valid and here with noTty, you're out!
369
- this.clearScreen(); // clear before leaving, not calling exit() since it may be a bad pattern to continue, devs should try/catch
370
- throw new Error('Missing required arguments. Please provide all required parameters.');
371
- }
372
- continue;
373
- }
374
- // If input passes validation and is not empty, or not required, move to the next question
375
- ctx.nextQuestion();
376
- }
377
- }
378
- return obj;
379
- }
380
- handleMissingArgsError(options) {
381
- this.clearScreen();
382
- if (options?.usageText) {
383
- this.log(options.usageText);
384
- }
385
- else if (options?.manPageInfo) {
386
- this.log(this.generateManPage(options.manPageInfo));
387
- }
388
- else {
389
- this.log('Missing required arguments. Please provide all required parameters.');
390
- }
391
- }
392
- hasMissingRequiredArgs(questions, argv) {
393
- return questions.some(question => question.required && this.isEmptyAnswer(argv[question.name]));
394
- }
395
- /**
396
- * Resolves the default value for a question using the resolver system.
397
- * Priority: defaultFrom > default > undefined
398
- */
399
- async resolveQuestionDefault(question) {
400
- // Try to resolve from defaultFrom first
401
- if ('defaultFrom' in question && question.defaultFrom) {
402
- const resolved = await this.resolverRegistry.resolve(question.defaultFrom);
403
- if (resolved !== undefined) {
404
- return resolved;
405
- }
406
- }
407
- // Fallback to static default
408
- if ('default' in question) {
409
- return question.default;
410
- }
411
- return undefined;
412
- }
413
- /**
414
- * Resolves dynamic defaults for all questions that have defaultFrom specified.
415
- * Updates the question.default property with the resolved value.
416
- */
417
- async resolveDynamicDefaults(questions) {
418
- for (const question of questions) {
419
- if ('defaultFrom' in question && question.defaultFrom) {
420
- const resolved = await this.resolveQuestionDefault(question);
421
- if (resolved !== undefined) {
422
- // Update question.default with resolved value
423
- question.default = resolved;
424
- }
425
- }
426
- }
427
- }
428
- /**
429
- * Resolves setFrom values for all questions that have setFrom specified.
430
- * Sets the value directly in obj, bypassing the prompt entirely.
431
- */
432
- async resolveSetValues(questions, obj) {
433
- for (const question of questions) {
434
- if ('setFrom' in question && question.setFrom) {
435
- // Only set if not already provided in args
436
- if (!(question.name in obj)) {
437
- const resolved = await this.resolverRegistry.resolve(question.setFrom);
438
- if (resolved !== undefined) {
439
- obj[question.name] = resolved;
440
- }
441
- }
442
- }
443
- }
444
- }
445
- /**
446
- * Resolves optionsFrom values for all questions that have optionsFrom specified.
447
- * Updates the question.options property with the resolved array.
448
- */
449
- async resolveOptionsFrom(questions) {
450
- for (const question of questions) {
451
- if ('optionsFrom' in question && question.optionsFrom) {
452
- const resolved = await this.resolverRegistry.resolve(question.optionsFrom);
453
- if (resolved !== undefined && Array.isArray(resolved)) {
454
- // Update question.options with resolved array
455
- question.options = resolved;
456
- }
457
- }
458
- }
459
- }
460
- /**
461
- * Extracts positional arguments from obj._ and assigns them to questions marked with _: true.
462
- *
463
- * Rules:
464
- * 1. Named arguments take precedence - if a question already has a value in obj, skip it
465
- * 2. Positional questions consume from obj._ left-to-right in declaration order
466
- * 3. Returns the count of consumed positionals so caller can strip them from obj._
467
- * 4. Missing positional values leave questions unset (for prompting/validation)
468
- *
469
- * This effectively allows "naming positional parameters" - users can pass values
470
- * without flags and they'll be assigned to the appropriate question names.
471
- *
472
- * @returns The number of positional arguments consumed
473
- */
474
- extractPositionalArgs(obj, questions) {
475
- // Get positional arguments array from obj (minimist convention)
476
- const positionals = Array.isArray(obj._) ? obj._ : [];
477
- if (positionals.length === 0) {
478
- return 0;
479
- }
480
- // Track which positional index we're consuming from
481
- let positionalIndex = 0;
482
- // Process questions in declaration order to maintain predictable assignment
483
- for (const question of questions) {
484
- // Only process questions marked as positional
485
- if (!question._) {
486
- continue;
487
- }
488
- // Skip if this question already has a named argument value
489
- // Named arguments always take precedence over positional
490
- if (question.name in obj && question.name !== '_') {
491
- continue;
492
- }
493
- // Skip if we've exhausted all positional arguments
494
- if (positionalIndex >= positionals.length) {
495
- break;
496
- }
497
- // Assign the next positional value to this question
498
- const value = positionals[positionalIndex];
499
- obj[question.name] = value;
500
- positionalIndex++;
501
- }
502
- return positionalIndex;
503
- }
504
- applyDefaultValues(questions, obj) {
505
- questions.forEach(question => {
506
- if ('default' in question) {
507
- obj[question.name] = question.default; // Set default value if specified
508
- }
509
- });
510
- }
511
- applyOverrides(argv, obj, questions) {
512
- questions.forEach(question => {
513
- if (question.name in argv) {
514
- this.handleOverrides(argv, obj, question);
515
- }
516
- });
517
- }
518
- handleOverrides(argv, obj, question) {
519
- if (!Object.prototype.hasOwnProperty.call(argv, question.name)) {
520
- return;
521
- }
522
- if (this.handledKeys.has(question.name)) {
523
- return; // Already handled, skip further processing
524
- }
525
- this.handledKeys.add(question.name);
526
- switch (question.type) {
527
- case 'text':
528
- case 'number':
529
- case 'confirm':
530
- // do nothing, already set!
531
- break;
532
- case 'checkbox':
533
- this.handleOverridesForCheckboxOptions(argv, obj, question);
534
- break;
535
- case 'autocomplete':
536
- case 'list':
537
- // get the value from options :)
538
- this.handleOverridesWithOptions(argv, obj, question);
539
- break;
540
- default:
541
- return;
542
- }
543
- }
544
- handleOverridesForCheckboxOptions(argv, obj, question) {
545
- const options = this.sanitizeOptions(question);
546
- const input = argv[question.name];
547
- // Normalize to array
548
- const inputs = Array.isArray(input) ? input.map(String) : [String(input)];
549
- // Set of matched values
550
- const inputSet = new Set(inputs);
551
- // Base list of processed options
552
- const result = options.map(opt => ({
553
- ...opt,
554
- selected: inputSet.has(opt.name) || inputSet.has(String(opt.value))
555
- }));
556
- // Add extras if allowed
557
- if (question.allowCustomOptions) {
558
- const knownValues = new Set(options.map(opt => String(opt.value)));
559
- const unknowns = inputs.filter(val => !knownValues.has(val));
560
- for (const val of unknowns) {
561
- result.push({
562
- name: val,
563
- value: val,
564
- selected: true
565
- });
566
- }
567
- }
568
- // Assign final result
569
- obj[question.name] = question.returnFullResults
570
- ? result
571
- : result.filter(opt => opt.selected);
572
- }
573
- handleOverridesWithOptions(argv, obj, question) {
574
- const input = argv[question.name];
575
- if (typeof input !== 'string')
576
- return;
577
- const options = this.sanitizeOptions(question);
578
- const found = options.find(opt => opt.name === input || String(opt.value) === input);
579
- if (found) {
580
- obj[question.name] = found.value;
581
- }
582
- else if (question.allowCustomOptions) {
583
- obj[question.name] = input; // Store as-is
584
- }
585
- }
586
- async handleQuestionType(question, ctx) {
587
- this.keypress?.clearHandlers();
588
- switch (question.type) {
589
- case 'confirm':
590
- return this.confirm(question, ctx);
591
- case 'checkbox':
592
- return this.checkbox(question, ctx);
593
- case 'list':
594
- return this.list(question, ctx);
595
- case 'autocomplete':
596
- return this.autocomplete(question, ctx);
597
- case 'number':
598
- return this.number(question, ctx);
599
- case 'text':
600
- return this.text(question, ctx);
601
- default:
602
- return this.text(question, ctx);
603
- }
604
- }
605
- async confirm(question, ctx) {
606
- if (this.noTty || !this.rl)
607
- return question.default ?? false; // Return default if non-interactive
608
- return new Promise((resolve) => {
609
- this.clearScreen();
610
- this.rl.question(this.getPrompt(question, ctx, ''), (answer) => {
611
- const userInput = answer.trim().toLowerCase();
612
- if (userInput === '') {
613
- resolve(question.default ?? false); // Use default value if input is empty
614
- }
615
- else if (['yes', 'y'].includes(userInput)) {
616
- resolve(true);
617
- }
618
- else {
619
- resolve(false);
620
- }
621
- });
622
- });
623
- }
624
- async text(question, ctx) {
625
- if (this.noTty || !this.rl) {
626
- if ('default' in question) {
627
- return question.default;
628
- }
629
- return;
630
- }
631
- let input = '';
632
- return new Promise((resolve) => {
633
- this.clearScreen();
634
- this.rl.question(this.getPrompt(question, ctx, input), (answer) => {
635
- input = answer;
636
- if (input.trim() !== '') {
637
- resolve(input); // Return input if not empty
638
- }
639
- else if ('default' in question) {
640
- resolve(question.default); // Use default if input is empty
641
- }
642
- else {
643
- resolve(null); // Return null if empty and not required
644
- }
645
- });
646
- });
647
- }
648
- async number(question, ctx) {
649
- if (this.noTty || !this.rl) {
650
- if ('default' in question) {
651
- return question.default;
652
- }
653
- return;
654
- }
655
- let input = '';
656
- return new Promise((resolve) => {
657
- this.clearScreen();
658
- this.rl.question(this.getPrompt(question, ctx, input), (answer) => {
659
- input = answer.trim();
660
- if (input !== '') {
661
- const num = Number(input);
662
- if (!isNaN(num)) {
663
- resolve(num);
664
- }
665
- else {
666
- resolve(null); // Let validation handle bad input
667
- }
668
- }
669
- else if ('default' in question) {
670
- resolve(question.default); // Use default if input is empty
671
- }
672
- else {
673
- resolve(null); // Empty and no default
674
- }
675
- });
676
- });
677
- }
678
- async checkbox(question, ctx) {
679
- if (this.noTty || !this.rl) {
680
- const options = this.sanitizeOptions(question);
681
- const defaults = Array.isArray(question.default)
682
- ? question.default
683
- : [question.default];
684
- // If returnFullResults is true, return all options with boolean selection
685
- if (question.returnFullResults) {
686
- return options.map(opt => ({
687
- name: opt.name,
688
- value: opt.value,
689
- selected: defaults.includes(opt.name)
690
- }));
691
- }
692
- // Otherwise, return only selected options
693
- return options
694
- .filter(opt => defaults.includes(opt.name) || defaults.includes(opt.value))
695
- .map(opt => ({
696
- name: opt.name,
697
- value: opt.value,
698
- selected: true
699
- }));
700
- }
701
- if (!question.options.length) {
702
- // no arguments don't make sense
703
- throw new Error('checkbox requires options');
704
- }
705
- this.keypress.resume();
706
- const options = this.sanitizeOptions(question);
707
- let input = ''; // Search input
708
- let filteredOptions = options;
709
- let selectedIndex = 0;
710
- let startIndex = 0; // Start index for visible options
711
- const maxLines = this.getMaxLines(question, options.length); // Use provided max or total options
712
- // const selections: boolean[] = new Array(options.length).fill(false);
713
- const selections = options.map(opt => {
714
- if (!question.default)
715
- return false;
716
- const defaults = Array.isArray(question.default)
717
- ? question.default
718
- : [question.default];
719
- return defaults.includes(opt.name);
720
- });
721
- const updateFilteredOptions = () => {
722
- filteredOptions = this.filterOptions(options, input);
723
- };
724
- const display = () => {
725
- this.clearScreen();
726
- this.displayPrompt(question, ctx, input);
727
- const endIndex = Math.min(startIndex + maxLines, filteredOptions.length);
728
- for (let i = startIndex; i < endIndex; i++) {
729
- const option = filteredOptions[i];
730
- const isSelected = selectedIndex === i;
731
- const marker = isSelected ? '>' : ' ';
732
- const index = options.map(o => o.name).indexOf(option.name);
733
- if (index >= 0) {
734
- const isChecked = selections[index] ? '◉' : '○'; // Use the original index in options
735
- const line = `${marker} ${isChecked} ${option.name}`;
736
- this.log(isSelected ? (0, yanse_1.blue)(line) : line);
737
- }
738
- else {
739
- this.log('No options'); // sometimes user searches and there are no options...
740
- }
741
- }
742
- };
743
- display();
744
- // Handling BACKSPACE key
745
- this.keypress.on(keypress_1.KEY_CODES.BACKSPACE, () => {
746
- input = input.slice(0, -1);
747
- updateFilteredOptions();
748
- display();
749
- });
750
- // Register alphanumeric keypresses to accumulate input, excluding space
751
- 'abcdefghijklmnopqrstuvwxyz0123456789'.split('').forEach(char => {
752
- this.keypress.on(char, () => {
753
- input += char;
754
- updateFilteredOptions();
755
- display();
756
- });
757
- });
758
- this.keypress.on(keypress_1.KEY_CODES.UP_ARROW, () => {
759
- selectedIndex = selectedIndex > 0 ? selectedIndex - 1 : filteredOptions.length - 1;
760
- if (selectedIndex < startIndex) {
761
- startIndex = selectedIndex; // Scroll up
762
- }
763
- else if (selectedIndex === filteredOptions.length - 1) {
764
- startIndex = Math.max(0, filteredOptions.length - maxLines); // Jump to the bottom of the list
765
- }
766
- display();
767
- });
768
- this.keypress.on(keypress_1.KEY_CODES.DOWN_ARROW, () => {
769
- selectedIndex = (selectedIndex + 1) % filteredOptions.length;
770
- if (selectedIndex >= startIndex + maxLines) {
771
- startIndex = selectedIndex - maxLines + 1; // Scroll down
772
- }
773
- else if (selectedIndex === 0) {
774
- startIndex = 0; // Jump to the top of the list
775
- }
776
- display();
777
- });
778
- this.keypress.on(keypress_1.KEY_CODES.SPACE, () => {
779
- // Map filtered index back to the original index in options
780
- selections[options.indexOf(filteredOptions[selectedIndex])] = !selections[options.indexOf(filteredOptions[selectedIndex])];
781
- display();
782
- });
783
- return new Promise(resolve => {
784
- this.keypress.on(keypress_1.KEY_CODES.ENTER, () => {
785
- this.keypress.pause();
786
- const result = [];
787
- if (question.returnFullResults) {
788
- // Return all options with their selected status
789
- options.forEach((option, index) => {
790
- result.push({
791
- name: option.name,
792
- value: option.value,
793
- selected: selections[index]
794
- });
795
- });
796
- }
797
- else {
798
- // Return only options that are selected
799
- options.forEach((option, index) => {
800
- if (selections[index]) {
801
- result.push({
802
- name: option.name,
803
- value: option.value,
804
- selected: selections[index]
805
- });
806
- }
807
- });
808
- }
809
- resolve(result);
810
- });
811
- });
812
- }
813
- async autocomplete(question, ctx) {
814
- if (this.noTty || !this.rl) {
815
- if ('default' in question) {
816
- return question.default;
817
- }
818
- return;
819
- }
820
- if (!question.options.length) {
821
- throw new Error('autocomplete requires options');
822
- }
823
- this.keypress.resume();
824
- const options = this.sanitizeOptions(question);
825
- let input = '';
826
- let filteredOptions = options;
827
- let selectedIndex = 0;
828
- let startIndex = 0; // Start index for visible options
829
- const maxLines = this.getMaxLines(question, options.length); // Use provided max or total options
830
- const display = () => {
831
- this.clearScreen();
832
- this.displayPrompt(question, ctx, input);
833
- // Determine the range of options to display
834
- const endIndex = Math.min(startIndex + maxLines, filteredOptions.length);
835
- for (let i = startIndex; i < endIndex; i++) {
836
- const option = filteredOptions[i];
837
- if (!option) {
838
- this.log('No options'); // sometimes user searches and there are no options...
839
- }
840
- else if (i === selectedIndex) {
841
- this.log((0, yanse_1.blue)('> ' + option.name)); // Highlight the selected option with yanse
842
- }
843
- else {
844
- this.log(' ' + option.name);
845
- }
846
- }
847
- };
848
- const updateFilteredOptions = () => {
849
- filteredOptions = this.filterOptions(options, input);
850
- // Adjust startIndex to keep the selectedIndex in the visible range
851
- if (selectedIndex < startIndex) {
852
- startIndex = selectedIndex;
853
- }
854
- else if (selectedIndex >= startIndex + maxLines) {
855
- startIndex = selectedIndex - maxLines + 1;
856
- }
857
- if (selectedIndex >= filteredOptions.length) {
858
- selectedIndex = Math.max(filteredOptions.length - 1, 0);
859
- }
860
- };
861
- display();
862
- // Handling BACKSPACE key
863
- this.keypress.on(keypress_1.KEY_CODES.BACKSPACE, () => {
864
- input = input.slice(0, -1);
865
- updateFilteredOptions();
866
- display();
867
- });
868
- // Register alphanumeric and space keypresses to accumulate input
869
- 'abcdefghijklmnopqrstuvwxyz0123456789 '.split('').forEach(char => {
870
- this.keypress.on(char, () => {
871
- input += char;
872
- updateFilteredOptions();
873
- display();
874
- });
875
- });
876
- // Navigation
877
- this.keypress.on(keypress_1.KEY_CODES.UP_ARROW, () => {
878
- selectedIndex = selectedIndex - 1 >= 0 ? selectedIndex - 1 : filteredOptions.length - 1;
879
- if (selectedIndex < startIndex) {
880
- startIndex = selectedIndex; // Scroll up
881
- }
882
- else if (selectedIndex === filteredOptions.length - 1) {
883
- startIndex = Math.max(0, filteredOptions.length - maxLines); // Jump to the bottom of the list
884
- }
885
- display();
886
- });
887
- this.keypress.on(keypress_1.KEY_CODES.DOWN_ARROW, () => {
888
- selectedIndex = (selectedIndex + 1) % filteredOptions.length;
889
- if (selectedIndex >= startIndex + maxLines) {
890
- startIndex = selectedIndex - maxLines + 1; // Scroll down
891
- }
892
- else if (selectedIndex === 0) {
893
- startIndex = 0; // Jump to the top of the list
894
- }
895
- display();
896
- });
897
- return new Promise(resolve => {
898
- this.keypress.on(keypress_1.KEY_CODES.ENTER, () => {
899
- this.keypress.pause();
900
- resolve(filteredOptions[selectedIndex]?.value || input);
901
- });
902
- });
903
- }
904
- async list(question, ctx) {
905
- if (this.noTty || !this.rl) {
906
- if ('default' in question) {
907
- return question.default;
908
- }
909
- return;
910
- }
911
- if (!question.options.length) {
912
- throw new Error('list requires options');
913
- }
914
- this.keypress.resume();
915
- const options = this.sanitizeOptions(question);
916
- let input = '';
917
- let selectedIndex = 0;
918
- let startIndex = 0; // Start index for visible options
919
- const maxLines = this.getMaxLines(question, options.length); // Use provided max or total options
920
- const display = () => {
921
- this.clearScreen();
922
- this.displayPrompt(question, ctx, input);
923
- // Determine the range of options to display
924
- const endIndex = Math.min(startIndex + maxLines, options.length);
925
- for (let i = startIndex; i < endIndex; i++) {
926
- const option = options[i];
927
- if (!option) {
928
- this.log('No options'); // sometimes user searches and there are no options...
929
- }
930
- else if (i === selectedIndex) {
931
- this.log((0, yanse_1.blue)('> ' + option.name)); // Highlight the selected option with yanse
932
- }
933
- else {
934
- this.log(' ' + option.name);
935
- }
936
- }
937
- };
938
- display();
939
- // Navigation
940
- this.keypress.on(keypress_1.KEY_CODES.UP_ARROW, () => {
941
- selectedIndex = selectedIndex - 1 >= 0 ? selectedIndex - 1 : options.length - 1;
942
- if (selectedIndex < startIndex) {
943
- startIndex = selectedIndex; // Scroll up
944
- }
945
- else if (selectedIndex === options.length - 1) {
946
- startIndex = Math.max(0, options.length - maxLines); // Jump to the bottom of the list
947
- }
948
- display();
949
- });
950
- this.keypress.on(keypress_1.KEY_CODES.DOWN_ARROW, () => {
951
- selectedIndex = (selectedIndex + 1) % options.length;
952
- if (selectedIndex >= startIndex + maxLines) {
953
- startIndex = selectedIndex - maxLines + 1; // Scroll down
954
- }
955
- else if (selectedIndex === 0) {
956
- startIndex = 0; // Jump to the top of the list
957
- }
958
- display();
959
- });
960
- return new Promise(resolve => {
961
- this.keypress.on(keypress_1.KEY_CODES.ENTER, () => {
962
- this.keypress.pause();
963
- resolve(options[selectedIndex]?.value || input);
964
- });
965
- });
966
- }
967
- getOptionValue(option) {
968
- if (typeof option === 'string') {
969
- return { name: option, value: option };
970
- }
971
- else if (typeof option === 'object' && option && 'name' in option) {
972
- return { name: option.name, value: option.value };
973
- }
974
- else {
975
- return undefined;
976
- }
977
- }
978
- sanitizeOptions(question) {
979
- const options = (question.options ?? []).map(option => this.getOptionValue(option));
980
- return options.filter(Boolean);
981
- }
982
- filterOptions(options, input) {
983
- input = input.toLowerCase(); // Normalize input for case-insensitive comparison
984
- // Fuzzy matching: Check if all characters of the input can be found in the option name in order
985
- const fuzzyMatch = (option, input) => {
986
- if (!input || !input.trim().length)
987
- return true;
988
- const length = input.length;
989
- let position = 0; // Position in the input string
990
- // Iterate over each character in the option name
991
- for (let i = 0; i < option.length; i++) {
992
- if (option[i] === input[position]) {
993
- position++; // Move to the next character in the input
994
- if (position === length) { // Check if we've matched all characters
995
- return true;
996
- }
997
- }
998
- }
999
- return false;
1000
- };
1001
- return options
1002
- .filter(option => fuzzyMatch(option.name.toLowerCase(), input))
1003
- .sort((a, b) => {
1004
- if (a.name < b.name) {
1005
- return -1;
1006
- }
1007
- if (a.name > b.name) {
1008
- return 1;
1009
- }
1010
- return 0;
1011
- });
1012
- }
1013
- getMaxLines(question, defaultLength) {
1014
- if (question.maxDisplayLines) {
1015
- return question.maxDisplayLines;
1016
- }
1017
- // if (!this.noTty && (this.output as any).isTTY) {
1018
- // const rows = Math.round(((this.output as any).rows ?? 0) / 7);
1019
- // return Math.max(rows, defaultLength);
1020
- // }
1021
- return Math.min(this.globalMaxLines, defaultLength);
1022
- }
1023
- // Method to cleanly close the readline interface
1024
- // NOTE: use exit() to close!
1025
- close() {
1026
- if (this.rl) {
1027
- this.rl.close();
1028
- this.keypress.destroy();
1029
- }
1030
- }
1031
- }
1032
- exports.Prompter = Prompter;