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