hedgequantx 1.8.49 → 2.3.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 (103) hide show
  1. package/README.md +13 -6
  2. package/bin/cli.js +13 -7
  3. package/dist/algo/copy-engine.js +3 -0
  4. package/dist/algo/copy-engine.jsc +0 -0
  5. package/dist/algo/engine.js +3 -0
  6. package/dist/algo/engine.jsc +0 -0
  7. package/dist/algo/market-data-rithmic.js +3 -0
  8. package/dist/algo/market-data-rithmic.jsc +0 -0
  9. package/dist/algo/market-data.js +3 -0
  10. package/dist/algo/market-data.jsc +0 -0
  11. package/dist/algo/rithmic/connection.js +3 -0
  12. package/dist/algo/rithmic/connection.jsc +0 -0
  13. package/dist/algo/rithmic/constants.js +3 -0
  14. package/dist/algo/rithmic/constants.jsc +0 -0
  15. package/dist/algo/rithmic/index.js +3 -0
  16. package/dist/algo/rithmic/index.jsc +0 -0
  17. package/dist/algo/rithmic/market-data.js +3 -0
  18. package/dist/algo/rithmic/market-data.jsc +0 -0
  19. package/dist/algo/rithmic/pnl.js +3 -0
  20. package/dist/algo/rithmic/pnl.jsc +0 -0
  21. package/dist/algo/rithmic/pool.js +3 -0
  22. package/dist/algo/rithmic/pool.jsc +0 -0
  23. package/dist/algo/rithmic/trading.js +3 -0
  24. package/dist/algo/rithmic/trading.jsc +0 -0
  25. package/dist/algo/rithmic-decoder.js +3 -0
  26. package/dist/algo/rithmic-decoder.jsc +0 -0
  27. package/dist/algo/strategies/ultra-scalping-v2.js +3 -0
  28. package/dist/algo/strategies/ultra-scalping-v2.jsc +0 -0
  29. package/dist/algo/strategies/ultra-scalping.js +3 -0
  30. package/dist/algo/strategies/ultra-scalping.jsc +0 -0
  31. package/dist/algo/trading-api-rithmic.js +3 -0
  32. package/dist/algo/trading-api-rithmic.jsc +0 -0
  33. package/dist/algo/trading-api.js +3 -0
  34. package/dist/algo/trading-api.jsc +0 -0
  35. package/dist/algo/utils/smart-logger.js +3 -0
  36. package/dist/algo/utils/smart-logger.jsc +0 -0
  37. package/dist/algo/utils/smart-logs.js +3 -0
  38. package/dist/algo/utils/smart-logs.jsc +0 -0
  39. package/package.json +33 -10
  40. package/protos/rithmic/account_pnl_position_update.proto +59 -0
  41. package/protos/rithmic/base.proto +7 -0
  42. package/protos/rithmic/best_bid_offer.proto +39 -0
  43. package/protos/rithmic/exchange_order_notification.proto +140 -0
  44. package/protos/rithmic/instrument_pnl_position_update.proto +50 -0
  45. package/protos/rithmic/last_trade.proto +53 -0
  46. package/protos/rithmic/request_account_list.proto +20 -0
  47. package/protos/rithmic/request_cancel_all_orders.proto +15 -0
  48. package/protos/rithmic/request_front_month_contract.proto +10 -0
  49. package/protos/rithmic/request_heartbeat.proto +13 -0
  50. package/protos/rithmic/request_login.proto +28 -0
  51. package/protos/rithmic/request_login_info.proto +10 -0
  52. package/protos/rithmic/request_logout.proto +10 -0
  53. package/protos/rithmic/request_market_data_update.proto +42 -0
  54. package/protos/rithmic/request_new_order.proto +84 -0
  55. package/protos/rithmic/request_pnl_position_snapshot.proto +14 -0
  56. package/protos/rithmic/request_pnl_position_updates.proto +20 -0
  57. package/protos/rithmic/request_product_codes.proto +9 -0
  58. package/protos/rithmic/request_rithmic_system_info.proto +8 -0
  59. package/protos/rithmic/request_show_order_history.proto +16 -0
  60. package/protos/rithmic/request_show_order_history_dates.proto +10 -0
  61. package/protos/rithmic/request_show_order_history_summary.proto +14 -0
  62. package/protos/rithmic/request_show_orders.proto +14 -0
  63. package/protos/rithmic/request_subscribe_for_order_updates.proto +14 -0
  64. package/protos/rithmic/request_tick_bar_replay.proto +48 -0
  65. package/protos/rithmic/request_trade_routes.proto +11 -0
  66. package/protos/rithmic/response_account_list.proto +18 -0
  67. package/protos/rithmic/response_front_month_contract.proto +13 -0
  68. package/protos/rithmic/response_heartbeat.proto +14 -0
  69. package/protos/rithmic/response_login.proto +18 -0
  70. package/protos/rithmic/response_login_info.proto +24 -0
  71. package/protos/rithmic/response_logout.proto +11 -0
  72. package/protos/rithmic/response_market_data_update.proto +9 -0
  73. package/protos/rithmic/response_new_order.proto +18 -0
  74. package/protos/rithmic/response_pnl_position_snapshot.proto +11 -0
  75. package/protos/rithmic/response_pnl_position_updates.proto +11 -0
  76. package/protos/rithmic/response_product_codes.proto +12 -0
  77. package/protos/rithmic/response_rithmic_system_info.proto +12 -0
  78. package/protos/rithmic/response_show_order_history.proto +11 -0
  79. package/protos/rithmic/response_show_order_history_dates.proto +13 -0
  80. package/protos/rithmic/response_show_order_history_summary.proto +11 -0
  81. package/protos/rithmic/response_show_orders.proto +11 -0
  82. package/protos/rithmic/response_subscribe_for_order_updates.proto +11 -0
  83. package/protos/rithmic/response_tick_bar_replay.proto +40 -0
  84. package/protos/rithmic/response_trade_routes.proto +19 -0
  85. package/protos/rithmic/rithmic_order_notification.proto +124 -0
  86. package/src/app.js +136 -89
  87. package/src/config/index.js +27 -8
  88. package/src/config/settings.js +155 -0
  89. package/src/pages/algo/copy-trading.js +293 -200
  90. package/src/pages/algo/one-account.js +1 -1
  91. package/src/security/encryption.js +81 -46
  92. package/src/security/index.js +12 -8
  93. package/src/security/rateLimit.js +68 -65
  94. package/src/security/validation.js +93 -79
  95. package/src/services/hqx-server.js +538 -206
  96. package/src/services/projectx/index.js +327 -204
  97. package/src/services/rithmic/index.js +288 -285
  98. package/src/services/session.js +184 -114
  99. package/src/services/tradovate/index.js +286 -297
  100. package/src/utils/http.js +236 -0
  101. package/src/utils/index.js +11 -2
  102. package/src/utils/logger.js +64 -33
  103. package/src/utils/prompts.js +79 -71
@@ -0,0 +1,236 @@
1
+ /**
2
+ * @fileoverview Shared HTTP client for all services
3
+ * @module utils/http
4
+ */
5
+
6
+ const https = require('https');
7
+ const http = require('http');
8
+ const { TIMEOUTS } = require('../config/settings');
9
+ const { logger } = require('./logger');
10
+
11
+ const log = logger.scope('HTTP');
12
+
13
+ /**
14
+ * @typedef {Object} HttpResponse
15
+ * @property {number} statusCode - HTTP status code
16
+ * @property {Object|string} data - Response body
17
+ * @property {Object} headers - Response headers
18
+ */
19
+
20
+ /**
21
+ * @typedef {Object} HttpOptions
22
+ * @property {string} [method='GET'] - HTTP method
23
+ * @property {Object} [headers] - Request headers
24
+ * @property {Object|string} [body] - Request body
25
+ * @property {number} [timeout] - Request timeout in ms
26
+ * @property {string} [token] - Bearer token for Authorization
27
+ * @property {string} [apiKey] - API key header
28
+ */
29
+
30
+ /**
31
+ * Performs an HTTP/HTTPS request
32
+ * @param {string} url - Full URL to request
33
+ * @param {HttpOptions} [options={}] - Request options
34
+ * @returns {Promise<HttpResponse>}
35
+ */
36
+ const request = (url, options = {}) => {
37
+ return new Promise((resolve, reject) => {
38
+ const {
39
+ method = 'GET',
40
+ headers = {},
41
+ body = null,
42
+ timeout = TIMEOUTS.API_REQUEST,
43
+ token = null,
44
+ apiKey = null,
45
+ } = options;
46
+
47
+ const parsedUrl = new URL(url);
48
+ const isHttps = parsedUrl.protocol === 'https:';
49
+ const client = isHttps ? https : http;
50
+
51
+ const postData = body ? (typeof body === 'string' ? body : JSON.stringify(body)) : null;
52
+
53
+ const reqOptions = {
54
+ hostname: parsedUrl.hostname,
55
+ port: parsedUrl.port || (isHttps ? 443 : 80),
56
+ path: parsedUrl.pathname + parsedUrl.search,
57
+ method,
58
+ headers: {
59
+ 'Content-Type': 'application/json',
60
+ 'Accept': 'application/json',
61
+ 'User-Agent': 'HQX-CLI/2.0.0',
62
+ ...headers,
63
+ },
64
+ timeout,
65
+ };
66
+
67
+ if (postData) {
68
+ reqOptions.headers['Content-Length'] = Buffer.byteLength(postData);
69
+ }
70
+
71
+ if (token) {
72
+ reqOptions.headers['Authorization'] = `Bearer ${token}`;
73
+ }
74
+
75
+ if (apiKey) {
76
+ reqOptions.headers['X-API-Key'] = apiKey;
77
+ }
78
+
79
+ log.debug(`${method} ${parsedUrl.pathname}`);
80
+
81
+ const req = client.request(reqOptions, (res) => {
82
+ let data = '';
83
+
84
+ res.on('data', chunk => { data += chunk; });
85
+
86
+ res.on('end', () => {
87
+ let parsed;
88
+ try {
89
+ parsed = JSON.parse(data);
90
+ } catch {
91
+ parsed = data;
92
+ }
93
+
94
+ log.debug(`Response ${res.statusCode}`, {
95
+ path: parsedUrl.pathname,
96
+ status: res.statusCode
97
+ });
98
+
99
+ resolve({
100
+ statusCode: res.statusCode,
101
+ data: parsed,
102
+ headers: res.headers,
103
+ });
104
+ });
105
+ });
106
+
107
+ req.on('error', (err) => {
108
+ log.error(`Request failed: ${err.message}`, { path: parsedUrl.pathname });
109
+ reject(err);
110
+ });
111
+
112
+ req.on('timeout', () => {
113
+ req.destroy();
114
+ const err = new Error(`Request timeout after ${timeout}ms`);
115
+ log.error(err.message, { path: parsedUrl.pathname });
116
+ reject(err);
117
+ });
118
+
119
+ if (postData) {
120
+ req.write(postData);
121
+ }
122
+
123
+ req.end();
124
+ });
125
+ };
126
+
127
+ /**
128
+ * Simplified request helper for common patterns
129
+ * @param {string} baseUrl - Base URL (e.g., 'https://api.example.com')
130
+ * @param {string} [basePath=''] - Base path prefix
131
+ * @returns {Object} Request methods bound to the base URL
132
+ */
133
+ const createClient = (baseUrl, basePath = '') => {
134
+ const buildUrl = (path) => `${baseUrl}${basePath}${path}`;
135
+
136
+ return {
137
+ /**
138
+ * GET request
139
+ * @param {string} path - Request path
140
+ * @param {HttpOptions} [options={}] - Options
141
+ */
142
+ get: (path, options = {}) => request(buildUrl(path), { ...options, method: 'GET' }),
143
+
144
+ /**
145
+ * POST request
146
+ * @param {string} path - Request path
147
+ * @param {Object} [body] - Request body
148
+ * @param {HttpOptions} [options={}] - Options
149
+ */
150
+ post: (path, body, options = {}) => request(buildUrl(path), { ...options, method: 'POST', body }),
151
+
152
+ /**
153
+ * PUT request
154
+ * @param {string} path - Request path
155
+ * @param {Object} [body] - Request body
156
+ * @param {HttpOptions} [options={}] - Options
157
+ */
158
+ put: (path, body, options = {}) => request(buildUrl(path), { ...options, method: 'PUT', body }),
159
+
160
+ /**
161
+ * DELETE request
162
+ * @param {string} path - Request path
163
+ * @param {HttpOptions} [options={}] - Options
164
+ */
165
+ delete: (path, options = {}) => request(buildUrl(path), { ...options, method: 'DELETE' }),
166
+
167
+ /**
168
+ * Sets authorization token for all subsequent requests
169
+ * @param {string} token - Bearer token
170
+ * @returns {Object} Client with token set
171
+ */
172
+ withToken: (token) => {
173
+ const client = createClient(baseUrl, basePath);
174
+ const wrapWithToken = (fn) => (path, bodyOrOpts, opts) => {
175
+ const options = opts || (typeof bodyOrOpts === 'object' && !bodyOrOpts?.method ? {} : bodyOrOpts) || {};
176
+ const body = opts ? bodyOrOpts : undefined;
177
+ return fn(path, body, { ...options, token });
178
+ };
179
+ client.get = (path, opts = {}) => request(buildUrl(path), { ...opts, method: 'GET', token });
180
+ client.post = (path, body, opts = {}) => request(buildUrl(path), { ...opts, method: 'POST', body, token });
181
+ client.put = (path, body, opts = {}) => request(buildUrl(path), { ...opts, method: 'PUT', body, token });
182
+ client.delete = (path, opts = {}) => request(buildUrl(path), { ...opts, method: 'DELETE', token });
183
+ return client;
184
+ },
185
+ };
186
+ };
187
+
188
+ /**
189
+ * Retries a request with exponential backoff
190
+ * @param {() => Promise<HttpResponse>} fn - Request function
191
+ * @param {Object} [options={}] - Retry options
192
+ * @param {number} [options.maxRetries=3] - Maximum retry attempts
193
+ * @param {number} [options.baseDelay=1000] - Base delay in ms
194
+ * @param {number[]} [options.retryOn=[502, 503, 504]] - Status codes to retry on
195
+ * @returns {Promise<HttpResponse>}
196
+ */
197
+ const withRetry = async (fn, options = {}) => {
198
+ const {
199
+ maxRetries = 3,
200
+ baseDelay = 1000,
201
+ retryOn = [502, 503, 504],
202
+ } = options;
203
+
204
+ let lastError;
205
+
206
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
207
+ try {
208
+ const result = await fn();
209
+
210
+ if (retryOn.includes(result.statusCode) && attempt < maxRetries) {
211
+ const delay = baseDelay * Math.pow(2, attempt);
212
+ log.debug(`Retrying in ${delay}ms (attempt ${attempt + 1}/${maxRetries})`);
213
+ await new Promise(r => setTimeout(r, delay));
214
+ continue;
215
+ }
216
+
217
+ return result;
218
+ } catch (err) {
219
+ lastError = err;
220
+
221
+ if (attempt < maxRetries) {
222
+ const delay = baseDelay * Math.pow(2, attempt);
223
+ log.debug(`Retrying after error in ${delay}ms`, { error: err.message });
224
+ await new Promise(r => setTimeout(r, delay));
225
+ }
226
+ }
227
+ }
228
+
229
+ throw lastError;
230
+ };
231
+
232
+ module.exports = {
233
+ request,
234
+ createClient,
235
+ withRetry,
236
+ };
@@ -1,8 +1,17 @@
1
1
  /**
2
- * Utils module exports
2
+ * @fileoverview Utils module exports
3
+ * @module utils
3
4
  */
4
5
 
5
6
  const { logger, LEVELS } = require('./logger');
6
7
  const prompts = require('./prompts');
8
+ const { request, createClient, withRetry } = require('./http');
7
9
 
8
- module.exports = { logger, LEVELS, prompts };
10
+ module.exports = {
11
+ logger,
12
+ LEVELS,
13
+ prompts,
14
+ request,
15
+ createClient,
16
+ withRetry,
17
+ };
@@ -1,70 +1,97 @@
1
1
  /**
2
- * HQX Logger - Centralized logging for debugging
2
+ * @fileoverview HQX Logger - Centralized logging for debugging
3
+ * @module utils/logger
3
4
  *
4
5
  * Usage:
5
6
  * HQX_DEBUG=1 hedgequantx - Enable all debug logs
6
- * HQX_LOG_FILE=1 hedgequantx - Also write to ~/.hedgequantx/debug.log
7
7
  */
8
8
 
9
9
  const fs = require('fs');
10
10
  const path = require('path');
11
11
  const os = require('os');
12
+ const { SECURITY, DEBUG } = require('../config/settings');
12
13
 
13
- // Log levels
14
+ /** Log levels */
14
15
  const LEVELS = {
15
16
  ERROR: 0,
16
17
  WARN: 1,
17
18
  INFO: 2,
18
19
  DEBUG: 3,
19
- TRACE: 4
20
+ TRACE: 4,
20
21
  };
21
22
 
22
- // Colors for console output
23
+ /** ANSI colors */
23
24
  const COLORS = {
24
- ERROR: '\x1b[31m', // Red
25
- WARN: '\x1b[33m', // Yellow
26
- INFO: '\x1b[36m', // Cyan
27
- DEBUG: '\x1b[90m', // Gray
28
- TRACE: '\x1b[90m', // Gray
29
- RESET: '\x1b[0m'
25
+ ERROR: '\x1b[31m',
26
+ WARN: '\x1b[33m',
27
+ INFO: '\x1b[36m',
28
+ DEBUG: '\x1b[90m',
29
+ TRACE: '\x1b[90m',
30
+ RESET: '\x1b[0m',
30
31
  };
31
32
 
33
+ /**
34
+ * Logger class with file and console output
35
+ */
32
36
  class Logger {
33
37
  constructor() {
34
- this.consoleEnabled = process.env.HQX_DEBUG === '1';
38
+ this.consoleEnabled = DEBUG.enabled;
35
39
  this.level = LEVELS.DEBUG;
36
- this.logFile = path.join(os.homedir(), '.hedgequantx', 'debug.log');
37
-
38
- // Always write to file (logs are always saved)
39
- const dir = path.dirname(this.logFile);
40
- if (!fs.existsSync(dir)) {
41
- fs.mkdirSync(dir, { recursive: true });
40
+ this.logDir = path.join(os.homedir(), SECURITY.SESSION_DIR);
41
+ this.logFile = path.join(this.logDir, DEBUG.LOG_FILE);
42
+ this._initLogFile();
43
+ }
44
+
45
+ /**
46
+ * Initialize log file
47
+ * @private
48
+ */
49
+ _initLogFile() {
50
+ try {
51
+ if (!fs.existsSync(this.logDir)) {
52
+ fs.mkdirSync(this.logDir, { recursive: true, mode: SECURITY.DIR_PERMISSIONS });
53
+ }
54
+
55
+ const header = [
56
+ `=== HQX Log Started ${new Date().toISOString()} ===`,
57
+ `Platform: ${process.platform}, Node: ${process.version}`,
58
+ `CWD: ${process.cwd()}`,
59
+ '',
60
+ ].join('\n');
61
+
62
+ fs.writeFileSync(this.logFile, header, { mode: SECURITY.FILE_PERMISSIONS });
63
+ } catch {
64
+ // Ignore init errors - logging is optional
42
65
  }
43
- // Clear log file on start
44
- fs.writeFileSync(this.logFile, `=== HQX Log Started ${new Date().toISOString()} ===\n`);
45
- fs.appendFileSync(this.logFile, `Platform: ${process.platform}, Node: ${process.version}\n`);
46
- fs.appendFileSync(this.logFile, `CWD: ${process.cwd()}\n\n`);
47
66
  }
48
67
 
68
+ /**
69
+ * Format log message
70
+ * @private
71
+ */
49
72
  _format(level, module, message, data) {
50
- const timestamp = new Date().toISOString().substr(11, 12); // HH:MM:SS.mmm
51
- const dataStr = data !== undefined ? ' ' + JSON.stringify(data) : '';
52
- return `[${timestamp}] [${level}] [${module}]${dataStr ? ' ' + message + dataStr : ' ' + message}`;
73
+ const timestamp = new Date().toISOString().slice(11, 23);
74
+ const dataStr = data !== undefined ? ` ${JSON.stringify(data)}` : '';
75
+ return `[${timestamp}] [${level}] [${module}] ${message}${dataStr}`;
53
76
  }
54
77
 
78
+ /**
79
+ * Write log entry
80
+ * @private
81
+ */
55
82
  _log(level, levelName, module, message, data) {
56
83
  if (level > this.level) return;
57
84
 
58
85
  const formatted = this._format(levelName, module, message, data);
59
-
60
- // Always write to file (survives crashes)
86
+
87
+ // Write to file (survives crashes)
61
88
  try {
62
89
  fs.appendFileSync(this.logFile, formatted + '\n');
63
- } catch (e) {
64
- // Ignore file write errors
90
+ } catch {
91
+ // Ignore file errors
65
92
  }
66
-
67
- // Console output only if HQX_DEBUG=1
93
+
94
+ // Console output only if enabled
68
95
  if (this.consoleEnabled) {
69
96
  const color = COLORS[levelName] || COLORS.RESET;
70
97
  console.error(`${color}${formatted}${COLORS.RESET}`);
@@ -91,7 +118,11 @@ class Logger {
91
118
  this._log(LEVELS.TRACE, 'TRACE', module, message, data);
92
119
  }
93
120
 
94
- // Create a scoped logger for a specific module
121
+ /**
122
+ * Create a scoped logger for a specific module
123
+ * @param {string} moduleName - Module name
124
+ * @returns {Object} Scoped logger methods
125
+ */
95
126
  scope(moduleName) {
96
127
  return {
97
128
  error: (msg, data) => this.error(moduleName, msg, data),
@@ -103,7 +134,7 @@ class Logger {
103
134
  }
104
135
  }
105
136
 
106
- // Singleton instance
137
+ /** Singleton instance */
107
138
  const logger = new Logger();
108
139
 
109
140
  module.exports = { logger, LEVELS };
@@ -1,33 +1,16 @@
1
1
  /**
2
- * Centralized prompts utility
2
+ * @fileoverview Centralized prompts utility
3
+ * @module utils/prompts
4
+ *
3
5
  * Uses native readline for reliable stdin handling
4
6
  */
5
7
 
6
8
  const inquirer = require('inquirer');
7
9
  const readline = require('readline');
8
10
 
9
- // Shared readline instance
11
+ /** @type {readline.Interface|null} */
10
12
  let rl = null;
11
13
 
12
- // Prevent readline from exiting on SIGINT during prompts
13
- process.on('SIGINT', () => {
14
- // Let the main app handle SIGINT
15
- });
16
-
17
- /**
18
- * Get or create readline interface
19
- */
20
- const getReadline = () => {
21
- if (!rl || rl.closed) {
22
- rl = readline.createInterface({
23
- input: process.stdin,
24
- output: process.stdout,
25
- terminal: true
26
- });
27
- }
28
- return rl;
29
- };
30
-
31
14
  /**
32
15
  * Ensure stdin is ready and flush any buffered input
33
16
  */
@@ -37,56 +20,68 @@ const prepareStdin = () => {
37
20
  if (process.stdin.isTTY && process.stdin.setRawMode) {
38
21
  process.stdin.setRawMode(false);
39
22
  }
40
- // Flush any buffered input by reading without waiting
41
23
  process.stdin.read();
42
- } catch (e) {}
24
+ } catch {
25
+ // Ignore stdin errors
26
+ }
27
+ };
28
+
29
+ /**
30
+ * Close existing readline if open
31
+ * @private
32
+ */
33
+ const closeReadline = () => {
34
+ if (rl && !rl.closed) {
35
+ try {
36
+ rl.close();
37
+ } catch {
38
+ // Ignore close errors
39
+ }
40
+ rl = null;
41
+ }
43
42
  };
44
43
 
45
44
  /**
46
45
  * Native readline prompt
46
+ * @param {string} message - Prompt message
47
+ * @returns {Promise<string>}
47
48
  */
48
49
  const nativePrompt = (message) => {
49
50
  return new Promise((resolve) => {
50
51
  try {
51
52
  prepareStdin();
52
-
53
- // Always create a fresh readline for each prompt to avoid state issues
54
- if (rl && !rl.closed) {
55
- try { rl.close(); } catch (e) {}
56
- rl = null;
57
- }
58
-
53
+ closeReadline();
54
+
59
55
  rl = readline.createInterface({
60
56
  input: process.stdin,
61
57
  output: process.stdout,
62
- terminal: true
58
+ terminal: true,
63
59
  });
64
-
60
+
65
61
  let answered = false;
66
-
67
- rl.question(message + ' ', (answer) => {
62
+
63
+ rl.question(`${message} `, (answer) => {
68
64
  answered = true;
69
- try { rl.close(); } catch (e) {}
70
- rl = null;
65
+ closeReadline();
71
66
  resolve(answer || '');
72
67
  });
73
-
74
- // Handle readline close (e.g., Ctrl+C)
68
+
75
69
  rl.on('close', () => {
76
70
  if (!answered) {
77
71
  rl = null;
78
72
  resolve('');
79
73
  }
80
74
  });
81
-
82
- } catch (e) {
75
+ } catch {
83
76
  resolve('');
84
77
  }
85
78
  });
86
79
  };
87
80
 
88
81
  /**
89
- * Wait for Enter
82
+ * Wait for Enter key
83
+ * @param {string} [message='Press Enter to continue...'] - Message to display
84
+ * @returns {Promise<void>}
90
85
  */
91
86
  const waitForEnter = async (message = 'Press Enter to continue...') => {
92
87
  await nativePrompt(message);
@@ -94,6 +89,9 @@ const waitForEnter = async (message = 'Press Enter to continue...') => {
94
89
 
95
90
  /**
96
91
  * Text input
92
+ * @param {string} message - Prompt message
93
+ * @param {string} [defaultVal=''] - Default value
94
+ * @returns {Promise<string>}
97
95
  */
98
96
  const textInput = async (message, defaultVal = '') => {
99
97
  const value = await nativePrompt(message);
@@ -101,48 +99,63 @@ const textInput = async (message, defaultVal = '') => {
101
99
  };
102
100
 
103
101
  /**
104
- * Password input
102
+ * Password input (masked)
103
+ * @param {string} message - Prompt message
104
+ * @returns {Promise<string>}
105
105
  */
106
106
  const passwordInput = async (message) => {
107
- if (rl && !rl.closed) { rl.close(); rl = null; }
107
+ closeReadline();
108
108
  prepareStdin();
109
+
109
110
  const { value } = await inquirer.prompt([{
110
111
  type: 'password',
111
112
  name: 'value',
112
113
  message,
113
114
  mask: '*',
114
- prefix: ''
115
+ prefix: '',
115
116
  }]);
117
+
116
118
  return value;
117
119
  };
118
120
 
119
121
  /**
120
- * Confirm - arrow keys selection
122
+ * Confirm prompt with arrow keys
123
+ * @param {string} message - Prompt message
124
+ * @param {boolean} [defaultVal=true] - Default value
125
+ * @returns {Promise<boolean>}
121
126
  */
122
127
  const confirmPrompt = async (message, defaultVal = true) => {
123
- if (rl && !rl.closed) { rl.close(); rl = null; }
128
+ closeReadline();
124
129
  prepareStdin();
125
- const choices = defaultVal
130
+
131
+ const choices = defaultVal
126
132
  ? [{ name: 'Yes', value: true }, { name: 'No', value: false }]
127
133
  : [{ name: 'No', value: false }, { name: 'Yes', value: true }];
128
-
134
+
129
135
  const { value } = await inquirer.prompt([{
130
136
  type: 'list',
131
137
  name: 'value',
132
138
  message,
133
139
  choices,
134
140
  prefix: '',
135
- loop: false
141
+ loop: false,
136
142
  }]);
143
+
137
144
  return value;
138
145
  };
139
146
 
140
147
  /**
141
- * Number input
148
+ * Number input with validation
149
+ * @param {string} message - Prompt message
150
+ * @param {number} [defaultVal=1] - Default value
151
+ * @param {number} [min=1] - Minimum value
152
+ * @param {number} [max=1000] - Maximum value
153
+ * @returns {Promise<number>}
142
154
  */
143
155
  const numberInput = async (message, defaultVal = 1, min = 1, max = 1000) => {
144
- if (rl && !rl.closed) { rl.close(); rl = null; }
156
+ closeReadline();
145
157
  prepareStdin();
158
+
146
159
  const { value } = await inquirer.prompt([{
147
160
  type: 'input',
148
161
  name: 'value',
@@ -150,39 +163,34 @@ const numberInput = async (message, defaultVal = 1, min = 1, max = 1000) => {
150
163
  default: String(defaultVal),
151
164
  prefix: '',
152
165
  validate: (v) => {
153
- const n = parseInt(v);
166
+ const n = parseInt(v, 10);
154
167
  if (isNaN(n)) return 'Enter a number';
155
168
  if (n < min) return `Min: ${min}`;
156
169
  if (n > max) return `Max: ${max}`;
157
170
  return true;
158
- }
171
+ },
159
172
  }]);
160
- return parseInt(value) || defaultVal;
173
+
174
+ return parseInt(value, 10) || defaultVal;
161
175
  };
162
176
 
163
177
  /**
164
- * Select - arrow keys navigation
165
- * Supports disabled options (separators) via opt.disabled
178
+ * Select from options with arrow keys
179
+ * @param {string} message - Prompt message
180
+ * @param {Array<{label: string, value: any, disabled?: boolean}>} options - Options
181
+ * @returns {Promise<any>}
166
182
  */
167
183
  const selectOption = async (message, options) => {
168
- // Close shared readline before inquirer to avoid conflicts
169
- if (rl && !rl.closed) {
170
- rl.close();
171
- rl = null;
172
- }
184
+ closeReadline();
173
185
  prepareStdin();
174
-
186
+
175
187
  const choices = options.map(opt => {
176
188
  if (opt.disabled) {
177
- // Use inquirer Separator for disabled items (category headers)
178
189
  return new inquirer.Separator(opt.label);
179
190
  }
180
- return {
181
- name: opt.label,
182
- value: opt.value
183
- };
191
+ return { name: opt.label, value: opt.value };
184
192
  });
185
-
193
+
186
194
  const { value } = await inquirer.prompt([{
187
195
  type: 'list',
188
196
  name: 'value',
@@ -190,9 +198,9 @@ const selectOption = async (message, options) => {
190
198
  choices,
191
199
  prefix: '',
192
200
  loop: false,
193
- pageSize: 20 // Increased to show more symbols
201
+ pageSize: 20,
194
202
  }]);
195
-
203
+
196
204
  return value;
197
205
  };
198
206
 
@@ -203,5 +211,5 @@ module.exports = {
203
211
  passwordInput,
204
212
  confirmPrompt,
205
213
  numberInput,
206
- selectOption
214
+ selectOption,
207
215
  };