hedgequantx 1.8.49 → 2.3.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.
- package/README.md +7 -6
- package/bin/cli.js +13 -7
- package/dist/algo/copy-engine.js +3 -0
- package/dist/algo/copy-engine.jsc +0 -0
- package/dist/algo/engine.js +3 -0
- package/dist/algo/engine.jsc +0 -0
- package/dist/algo/market-data-rithmic.js +3 -0
- package/dist/algo/market-data-rithmic.jsc +0 -0
- package/dist/algo/market-data.js +3 -0
- package/dist/algo/market-data.jsc +0 -0
- package/dist/algo/rithmic/connection.js +3 -0
- package/dist/algo/rithmic/connection.jsc +0 -0
- package/dist/algo/rithmic/constants.js +3 -0
- package/dist/algo/rithmic/constants.jsc +0 -0
- package/dist/algo/rithmic/index.js +3 -0
- package/dist/algo/rithmic/index.jsc +0 -0
- package/dist/algo/rithmic/market-data.js +3 -0
- package/dist/algo/rithmic/market-data.jsc +0 -0
- package/dist/algo/rithmic/pnl.js +3 -0
- package/dist/algo/rithmic/pnl.jsc +0 -0
- package/dist/algo/rithmic/pool.js +3 -0
- package/dist/algo/rithmic/pool.jsc +0 -0
- package/dist/algo/rithmic/trading.js +3 -0
- package/dist/algo/rithmic/trading.jsc +0 -0
- package/dist/algo/rithmic-decoder.js +3 -0
- package/dist/algo/rithmic-decoder.jsc +0 -0
- package/dist/algo/strategies/ultra-scalping-v2.js +3 -0
- package/dist/algo/strategies/ultra-scalping-v2.jsc +0 -0
- package/dist/algo/strategies/ultra-scalping.js +3 -0
- package/dist/algo/strategies/ultra-scalping.jsc +0 -0
- package/dist/algo/trading-api-rithmic.js +3 -0
- package/dist/algo/trading-api-rithmic.jsc +0 -0
- package/dist/algo/trading-api.js +3 -0
- package/dist/algo/trading-api.jsc +0 -0
- package/dist/algo/utils/smart-logger.js +3 -0
- package/dist/algo/utils/smart-logger.jsc +0 -0
- package/dist/algo/utils/smart-logs.js +3 -0
- package/dist/algo/utils/smart-logs.jsc +0 -0
- package/package.json +33 -10
- package/protos/rithmic/account_pnl_position_update.proto +59 -0
- package/protos/rithmic/base.proto +7 -0
- package/protos/rithmic/best_bid_offer.proto +39 -0
- package/protos/rithmic/exchange_order_notification.proto +140 -0
- package/protos/rithmic/instrument_pnl_position_update.proto +50 -0
- package/protos/rithmic/last_trade.proto +53 -0
- package/protos/rithmic/request_account_list.proto +20 -0
- package/protos/rithmic/request_cancel_all_orders.proto +15 -0
- package/protos/rithmic/request_front_month_contract.proto +10 -0
- package/protos/rithmic/request_heartbeat.proto +13 -0
- package/protos/rithmic/request_login.proto +28 -0
- package/protos/rithmic/request_login_info.proto +10 -0
- package/protos/rithmic/request_logout.proto +10 -0
- package/protos/rithmic/request_market_data_update.proto +42 -0
- package/protos/rithmic/request_new_order.proto +84 -0
- package/protos/rithmic/request_pnl_position_snapshot.proto +14 -0
- package/protos/rithmic/request_pnl_position_updates.proto +20 -0
- package/protos/rithmic/request_product_codes.proto +9 -0
- package/protos/rithmic/request_rithmic_system_info.proto +8 -0
- package/protos/rithmic/request_show_order_history.proto +16 -0
- package/protos/rithmic/request_show_order_history_dates.proto +10 -0
- package/protos/rithmic/request_show_order_history_summary.proto +14 -0
- package/protos/rithmic/request_show_orders.proto +14 -0
- package/protos/rithmic/request_subscribe_for_order_updates.proto +14 -0
- package/protos/rithmic/request_tick_bar_replay.proto +48 -0
- package/protos/rithmic/request_trade_routes.proto +11 -0
- package/protos/rithmic/response_account_list.proto +18 -0
- package/protos/rithmic/response_front_month_contract.proto +13 -0
- package/protos/rithmic/response_heartbeat.proto +14 -0
- package/protos/rithmic/response_login.proto +18 -0
- package/protos/rithmic/response_login_info.proto +24 -0
- package/protos/rithmic/response_logout.proto +11 -0
- package/protos/rithmic/response_market_data_update.proto +9 -0
- package/protos/rithmic/response_new_order.proto +18 -0
- package/protos/rithmic/response_pnl_position_snapshot.proto +11 -0
- package/protos/rithmic/response_pnl_position_updates.proto +11 -0
- package/protos/rithmic/response_product_codes.proto +12 -0
- package/protos/rithmic/response_rithmic_system_info.proto +12 -0
- package/protos/rithmic/response_show_order_history.proto +11 -0
- package/protos/rithmic/response_show_order_history_dates.proto +13 -0
- package/protos/rithmic/response_show_order_history_summary.proto +11 -0
- package/protos/rithmic/response_show_orders.proto +11 -0
- package/protos/rithmic/response_subscribe_for_order_updates.proto +11 -0
- package/protos/rithmic/response_tick_bar_replay.proto +40 -0
- package/protos/rithmic/response_trade_routes.proto +19 -0
- package/protos/rithmic/rithmic_order_notification.proto +124 -0
- package/src/app.js +136 -89
- package/src/config/index.js +27 -8
- package/src/config/settings.js +155 -0
- package/src/pages/algo/copy-trading.js +293 -200
- package/src/pages/algo/one-account.js +1 -1
- package/src/security/encryption.js +81 -46
- package/src/security/index.js +12 -8
- package/src/security/rateLimit.js +68 -65
- package/src/security/validation.js +93 -79
- package/src/services/hqx-server.js +538 -206
- package/src/services/projectx/index.js +327 -204
- package/src/services/rithmic/index.js +288 -285
- package/src/services/session.js +184 -114
- package/src/services/tradovate/index.js +286 -297
- package/src/utils/http.js +236 -0
- package/src/utils/index.js +11 -2
- package/src/utils/logger.js +64 -33
- 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
|
+
};
|
package/src/utils/index.js
CHANGED
|
@@ -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 = {
|
|
10
|
+
module.exports = {
|
|
11
|
+
logger,
|
|
12
|
+
LEVELS,
|
|
13
|
+
prompts,
|
|
14
|
+
request,
|
|
15
|
+
createClient,
|
|
16
|
+
withRetry,
|
|
17
|
+
};
|
package/src/utils/logger.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
23
|
+
/** ANSI colors */
|
|
23
24
|
const COLORS = {
|
|
24
|
-
ERROR: '\x1b[31m',
|
|
25
|
-
WARN: '\x1b[33m',
|
|
26
|
-
INFO: '\x1b[36m',
|
|
27
|
-
DEBUG: '\x1b[90m',
|
|
28
|
-
TRACE: '\x1b[90m',
|
|
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 =
|
|
38
|
+
this.consoleEnabled = DEBUG.enabled;
|
|
35
39
|
this.level = LEVELS.DEBUG;
|
|
36
|
-
this.
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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().
|
|
51
|
-
const dataStr = data !== undefined ?
|
|
52
|
-
return `[${timestamp}] [${level}] [${module}]${
|
|
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
|
-
//
|
|
86
|
+
|
|
87
|
+
// Write to file (survives crashes)
|
|
61
88
|
try {
|
|
62
89
|
fs.appendFileSync(this.logFile, formatted + '\n');
|
|
63
|
-
} catch
|
|
64
|
-
// Ignore file
|
|
90
|
+
} catch {
|
|
91
|
+
// Ignore file errors
|
|
65
92
|
}
|
|
66
|
-
|
|
67
|
-
// Console output only if
|
|
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
|
-
|
|
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
|
-
|
|
137
|
+
/** Singleton instance */
|
|
107
138
|
const logger = new Logger();
|
|
108
139
|
|
|
109
140
|
module.exports = { logger, LEVELS };
|
package/src/utils/prompts.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
62
|
+
|
|
63
|
+
rl.question(`${message} `, (answer) => {
|
|
68
64
|
answered = true;
|
|
69
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
128
|
+
closeReadline();
|
|
124
129
|
prepareStdin();
|
|
125
|
-
|
|
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
|
-
|
|
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
|
-
|
|
173
|
+
|
|
174
|
+
return parseInt(value, 10) || defaultVal;
|
|
161
175
|
};
|
|
162
176
|
|
|
163
177
|
/**
|
|
164
|
-
* Select
|
|
165
|
-
*
|
|
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
|
-
|
|
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
|
|
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
|
};
|