hedgequantx 2.9.57 → 2.9.59

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.
@@ -1,42 +1,18 @@
1
1
  /**
2
- * @fileoverview Centralized prompts utility
2
+ * @fileoverview Centralized prompts utility with animated yellow spinner
3
3
  * @module utils/prompts
4
4
  *
5
- * Uses native readline for reliable stdin handling
6
- * Yellow spinner shows activity while waiting for user input
5
+ * Custom readline-based prompts with animated spinner that runs
6
+ * while waiting for user input. Uses inquirer only for complex
7
+ * prompts (password, list selection).
7
8
  */
8
9
 
9
10
  const inquirer = require('inquirer');
10
11
  const readline = require('readline');
11
12
  const chalk = require('chalk');
12
13
 
13
- // Spinner characters for yellow waiting indicator
14
+ // Spinner frames for yellow waiting indicator
14
15
  const SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
15
- let spinnerInterval = null;
16
- let spinnerFrame = 0;
17
-
18
- /**
19
- * Start yellow spinner to show we're waiting for user input
20
- */
21
- const startSpinner = () => {
22
- if (spinnerInterval) return;
23
- spinnerFrame = 0;
24
- spinnerInterval = setInterval(() => {
25
- spinnerFrame = (spinnerFrame + 1) % SPINNER_FRAMES.length;
26
- process.stdout.write(`\r${chalk.yellow(SPINNER_FRAMES[spinnerFrame])} `);
27
- }, 80);
28
- };
29
-
30
- /**
31
- * Stop spinner and clear line
32
- */
33
- const stopSpinner = () => {
34
- if (spinnerInterval) {
35
- clearInterval(spinnerInterval);
36
- spinnerInterval = null;
37
- process.stdout.write('\r \r'); // Clear spinner
38
- }
39
- };
40
16
 
41
17
  /** @type {readline.Interface|null} */
42
18
  let rl = null;
@@ -72,64 +48,305 @@ const closeReadline = () => {
72
48
  };
73
49
 
74
50
  /**
75
- * Native readline prompt
51
+ * Animated spinner prompt using raw readline
52
+ * Spinner animates while waiting for user input
76
53
  * @param {string} message - Prompt message
77
54
  * @returns {Promise<string>}
78
55
  */
79
- const nativePrompt = (message) => {
56
+ const animatedPrompt = (message) => {
80
57
  return new Promise((resolve) => {
81
- try {
82
- prepareStdin();
83
- closeReadline();
58
+ prepareStdin();
59
+ closeReadline();
84
60
 
85
- rl = readline.createInterface({
86
- input: process.stdin,
87
- output: process.stdout,
88
- terminal: true,
89
- });
61
+ let frameIndex = 0;
62
+ let userInput = '';
63
+ let cursorPos = 0;
90
64
 
91
- let answered = false;
65
+ // Enable raw mode for character-by-character input
66
+ if (process.stdin.isTTY) {
67
+ process.stdin.setRawMode(true);
68
+ }
69
+ process.stdin.resume();
92
70
 
93
- rl.question(`${message} `, (answer) => {
94
- answered = true;
95
- closeReadline();
96
- resolve(answer || '');
97
- });
71
+ const render = () => {
72
+ const spinner = chalk.yellow(SPINNER_FRAMES[frameIndex]);
73
+ const line = `\r${spinner} ${message} ${userInput}`;
74
+ process.stdout.write('\r\x1b[K'); // Clear line
75
+ process.stdout.write(line);
76
+ };
77
+
78
+ // Animate spinner every 80ms
79
+ const spinnerInterval = setInterval(() => {
80
+ frameIndex = (frameIndex + 1) % SPINNER_FRAMES.length;
81
+ render();
82
+ }, 80);
83
+
84
+ render();
98
85
 
99
- rl.on('close', () => {
100
- if (!answered) {
101
- rl = null;
102
- resolve('');
86
+ const onData = (key) => {
87
+ const char = key.toString();
88
+
89
+ // Enter key
90
+ if (char === '\r' || char === '\n') {
91
+ clearInterval(spinnerInterval);
92
+ process.stdin.removeListener('data', onData);
93
+ if (process.stdin.isTTY) {
94
+ process.stdin.setRawMode(false);
103
95
  }
104
- });
105
- } catch {
106
- resolve('');
96
+ process.stdout.write('\n');
97
+ resolve(userInput);
98
+ return;
99
+ }
100
+
101
+ // Ctrl+C
102
+ if (char === '\x03') {
103
+ clearInterval(spinnerInterval);
104
+ process.stdin.removeListener('data', onData);
105
+ if (process.stdin.isTTY) {
106
+ process.stdin.setRawMode(false);
107
+ }
108
+ process.stdout.write('\n');
109
+ process.exit(0);
110
+ }
111
+
112
+ // Backspace
113
+ if (char === '\x7f' || char === '\b') {
114
+ if (userInput.length > 0) {
115
+ userInput = userInput.slice(0, -1);
116
+ render();
117
+ }
118
+ return;
119
+ }
120
+
121
+ // Regular printable character
122
+ if (char >= ' ' && char <= '~') {
123
+ userInput += char;
124
+ render();
125
+ }
126
+ };
127
+
128
+ process.stdin.on('data', onData);
129
+ });
130
+ };
131
+
132
+ /**
133
+ * Animated Y/N confirm prompt
134
+ * Shows [Y/n] or [y/N] based on default
135
+ * @param {string} message - Prompt message
136
+ * @param {boolean} defaultVal - Default value
137
+ * @returns {Promise<boolean>}
138
+ */
139
+ const animatedConfirm = (message, defaultVal = true) => {
140
+ return new Promise((resolve) => {
141
+ prepareStdin();
142
+ closeReadline();
143
+
144
+ let frameIndex = 0;
145
+ const hint = defaultVal ? '[Y/n]' : '[y/N]';
146
+
147
+ if (process.stdin.isTTY) {
148
+ process.stdin.setRawMode(true);
149
+ }
150
+ process.stdin.resume();
151
+
152
+ const render = () => {
153
+ const spinner = chalk.yellow(SPINNER_FRAMES[frameIndex]);
154
+ process.stdout.write('\r\x1b[K');
155
+ process.stdout.write(`${spinner} ${message} ${chalk.dim(hint)} `);
156
+ };
157
+
158
+ const spinnerInterval = setInterval(() => {
159
+ frameIndex = (frameIndex + 1) % SPINNER_FRAMES.length;
160
+ render();
161
+ }, 80);
162
+
163
+ render();
164
+
165
+ const onData = (key) => {
166
+ const char = key.toString().toLowerCase();
167
+
168
+ // Enter = use default
169
+ if (char === '\r' || char === '\n') {
170
+ cleanup();
171
+ process.stdout.write(defaultVal ? 'Yes' : 'No');
172
+ process.stdout.write('\n');
173
+ resolve(defaultVal);
174
+ return;
175
+ }
176
+
177
+ // Y = yes
178
+ if (char === 'y') {
179
+ cleanup();
180
+ process.stdout.write('Yes');
181
+ process.stdout.write('\n');
182
+ resolve(true);
183
+ return;
184
+ }
185
+
186
+ // N = no
187
+ if (char === 'n') {
188
+ cleanup();
189
+ process.stdout.write('No');
190
+ process.stdout.write('\n');
191
+ resolve(false);
192
+ return;
193
+ }
194
+
195
+ // Ctrl+C
196
+ if (char === '\x03') {
197
+ cleanup();
198
+ process.stdout.write('\n');
199
+ process.exit(0);
200
+ }
201
+ };
202
+
203
+ const cleanup = () => {
204
+ clearInterval(spinnerInterval);
205
+ process.stdin.removeListener('data', onData);
206
+ if (process.stdin.isTTY) {
207
+ process.stdin.setRawMode(false);
208
+ }
209
+ };
210
+
211
+ process.stdin.on('data', onData);
212
+ });
213
+ };
214
+
215
+ /**
216
+ * Animated list selection with arrow keys
217
+ * @param {string} message - Prompt message
218
+ * @param {Array<{name: string, value: any}>} choices - Options
219
+ * @returns {Promise<any>}
220
+ */
221
+ const animatedSelect = (message, choices) => {
222
+ return new Promise((resolve) => {
223
+ prepareStdin();
224
+ closeReadline();
225
+
226
+ let frameIndex = 0;
227
+ let selectedIndex = 0;
228
+ const validChoices = choices.filter(c => !c.disabled);
229
+
230
+ if (process.stdin.isTTY) {
231
+ process.stdin.setRawMode(true);
107
232
  }
233
+ process.stdin.resume();
234
+
235
+ const render = () => {
236
+ const spinner = chalk.yellow(SPINNER_FRAMES[frameIndex]);
237
+ // Move cursor to start and clear down
238
+ process.stdout.write('\r\x1b[K');
239
+ process.stdout.write(`${spinner} ${message}\n`);
240
+
241
+ validChoices.forEach((choice, i) => {
242
+ process.stdout.write('\x1b[K'); // Clear line
243
+ if (i === selectedIndex) {
244
+ process.stdout.write(`${chalk.cyan('❯')} ${chalk.cyan(choice.name)}\n`);
245
+ } else {
246
+ process.stdout.write(` ${choice.name}\n`);
247
+ }
248
+ });
249
+
250
+ // Move cursor back up
251
+ process.stdout.write(`\x1b[${validChoices.length + 1}A`);
252
+ };
253
+
254
+ const spinnerInterval = setInterval(() => {
255
+ frameIndex = (frameIndex + 1) % SPINNER_FRAMES.length;
256
+ render();
257
+ }, 80);
258
+
259
+ render();
260
+
261
+ let escapeSeq = '';
262
+
263
+ const onData = (key) => {
264
+ const char = key.toString();
265
+
266
+ // Handle escape sequences (arrow keys)
267
+ if (char === '\x1b') {
268
+ escapeSeq = char;
269
+ return;
270
+ }
271
+
272
+ if (escapeSeq === '\x1b' && char === '[') {
273
+ escapeSeq += char;
274
+ return;
275
+ }
276
+
277
+ if (escapeSeq === '\x1b[') {
278
+ escapeSeq = '';
279
+ // Up arrow
280
+ if (char === 'A') {
281
+ selectedIndex = Math.max(0, selectedIndex - 1);
282
+ render();
283
+ return;
284
+ }
285
+ // Down arrow
286
+ if (char === 'B') {
287
+ selectedIndex = Math.min(validChoices.length - 1, selectedIndex + 1);
288
+ render();
289
+ return;
290
+ }
291
+ }
292
+
293
+ // Enter = select
294
+ if (char === '\r' || char === '\n') {
295
+ cleanup();
296
+ // Clear the menu lines
297
+ process.stdout.write('\r\x1b[K');
298
+ for (let i = 0; i < validChoices.length; i++) {
299
+ process.stdout.write('\x1b[B\x1b[K');
300
+ }
301
+ process.stdout.write(`\x1b[${validChoices.length}A`);
302
+ process.stdout.write(`${chalk.yellow('⠋')} ${message} ${chalk.cyan(validChoices[selectedIndex].name)}\n`);
303
+ resolve(validChoices[selectedIndex].value);
304
+ return;
305
+ }
306
+
307
+ // Ctrl+C
308
+ if (char === '\x03') {
309
+ cleanup();
310
+ process.stdout.write('\n');
311
+ process.exit(0);
312
+ }
313
+ };
314
+
315
+ const cleanup = () => {
316
+ clearInterval(spinnerInterval);
317
+ process.stdin.removeListener('data', onData);
318
+ if (process.stdin.isTTY) {
319
+ process.stdin.setRawMode(false);
320
+ }
321
+ };
322
+
323
+ process.stdin.on('data', onData);
108
324
  });
109
325
  };
110
326
 
111
327
  /**
112
- * Wait for Enter key + yellow spinner
328
+ * Wait for Enter key with animated spinner
113
329
  * @param {string} [message='Press Enter to continue...'] - Message to display
114
330
  * @returns {Promise<void>}
115
331
  */
116
332
  const waitForEnter = async (message = 'Press Enter to continue...') => {
117
- await nativePrompt(`${chalk.yellow('⠋')} ${message}`);
333
+ await animatedPrompt(message);
118
334
  };
119
335
 
120
336
  /**
121
- * Text input + yellow spinner
337
+ * Text input with animated spinner
122
338
  * @param {string} message - Prompt message
123
339
  * @param {string} [defaultVal=''] - Default value
124
340
  * @returns {Promise<string>}
125
341
  */
126
342
  const textInput = async (message, defaultVal = '') => {
127
- const value = await nativePrompt(`${chalk.yellow('⠋')} ${message}`);
343
+ const displayMsg = defaultVal ? `${message} (${defaultVal})` : message;
344
+ const value = await animatedPrompt(displayMsg);
128
345
  return value || defaultVal;
129
346
  };
130
347
 
131
348
  /**
132
- * Password input (masked) + yellow spinner
349
+ * Password input (masked) - uses inquirer for masking
133
350
  * @param {string} message - Prompt message
134
351
  * @returns {Promise<string>}
135
352
  */
@@ -149,33 +366,17 @@ const passwordInput = async (message) => {
149
366
  };
150
367
 
151
368
  /**
152
- * Confirm prompt with arrow keys + yellow spinner
369
+ * Confirm prompt with animated spinner (Y/n)
153
370
  * @param {string} message - Prompt message
154
371
  * @param {boolean} [defaultVal=true] - Default value
155
372
  * @returns {Promise<boolean>}
156
373
  */
157
374
  const confirmPrompt = async (message, defaultVal = true) => {
158
- closeReadline();
159
- prepareStdin();
160
-
161
- const choices = defaultVal
162
- ? [{ name: 'Yes', value: true }, { name: 'No', value: false }]
163
- : [{ name: 'No', value: false }, { name: 'Yes', value: true }];
164
-
165
- const { value } = await inquirer.prompt([{
166
- type: 'list',
167
- name: 'value',
168
- message: `${chalk.yellow('⠋')} ${message}`,
169
- choices,
170
- prefix: '',
171
- loop: false,
172
- }]);
173
-
174
- return value;
375
+ return animatedConfirm(message, defaultVal);
175
376
  };
176
377
 
177
378
  /**
178
- * Number input with validation + yellow spinner
379
+ * Number input with animated spinner and validation
179
380
  * @param {string} message - Prompt message
180
381
  * @param {number} [defaultVal=1] - Default value
181
382
  * @param {number} [min=1] - Minimum value
@@ -183,55 +384,40 @@ const confirmPrompt = async (message, defaultVal = true) => {
183
384
  * @returns {Promise<number>}
184
385
  */
185
386
  const numberInput = async (message, defaultVal = 1, min = 1, max = 1000) => {
186
- closeReadline();
187
- prepareStdin();
188
-
189
- const { value } = await inquirer.prompt([{
190
- type: 'input',
191
- name: 'value',
192
- message: `${chalk.yellow('⠋')} ${message}`,
193
- default: String(defaultVal),
194
- prefix: '',
195
- validate: (v) => {
196
- const n = parseInt(v, 10);
197
- if (isNaN(n)) return 'Enter a number';
198
- if (n < min) return `Min: ${min}`;
199
- if (n > max) return `Max: ${max}`;
200
- return true;
201
- },
202
- }]);
203
-
204
- return parseInt(value, 10) || defaultVal;
387
+ const displayMsg = `${message} (${min}-${max}, default: ${defaultVal})`;
388
+
389
+ while (true) {
390
+ const value = await animatedPrompt(displayMsg);
391
+
392
+ if (!value) return defaultVal;
393
+
394
+ const num = parseInt(value, 10);
395
+ if (isNaN(num)) {
396
+ console.log(chalk.red('Please enter a valid number'));
397
+ continue;
398
+ }
399
+ if (num < min || num > max) {
400
+ console.log(chalk.red(`Please enter a number between ${min} and ${max}`));
401
+ continue;
402
+ }
403
+ return num;
404
+ }
205
405
  };
206
406
 
207
407
  /**
208
- * Select from options with arrow keys + yellow spinner
408
+ * Select from options with animated spinner and arrow keys
209
409
  * @param {string} message - Prompt message
210
410
  * @param {Array<{label: string, value: any, disabled?: boolean}>} options - Options
211
411
  * @returns {Promise<any>}
212
412
  */
213
413
  const selectOption = async (message, options) => {
214
- closeReadline();
215
- prepareStdin();
216
-
217
- const choices = options.map(opt => {
218
- if (opt.disabled) {
219
- return new inquirer.Separator(opt.label);
220
- }
221
- return { name: opt.label, value: opt.value };
222
- });
414
+ const choices = options.map(opt => ({
415
+ name: opt.label,
416
+ value: opt.value,
417
+ disabled: opt.disabled || false,
418
+ }));
223
419
 
224
- const { value } = await inquirer.prompt([{
225
- type: 'list',
226
- name: 'value',
227
- message: `${chalk.yellow('⠋')} ${message}`,
228
- choices,
229
- prefix: '',
230
- loop: false,
231
- pageSize: 20,
232
- }]);
233
-
234
- return value;
420
+ return animatedSelect(message, choices);
235
421
  };
236
422
 
237
423
  module.exports = {