hedgequantx 1.2.146 → 1.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.
@@ -0,0 +1,318 @@
1
+ /**
2
+ * Dashboard Menu - Main menu after login
3
+ * Shows connected PropFirms and navigation options
4
+ */
5
+
6
+ const chalk = require('chalk');
7
+ const inquirer = require('inquirer');
8
+ const ora = require('ora');
9
+ const { execSync, spawn } = require('child_process');
10
+
11
+ const { connections } = require('../services');
12
+ const { getLogoWidth, centerText, prepareStdin } = require('../ui');
13
+
14
+ /**
15
+ * Dashboard menu after login
16
+ * @param {Object} service - Connected service
17
+ */
18
+ const dashboardMenu = async (service) => {
19
+ // Ensure stdin is ready for prompts
20
+ prepareStdin();
21
+
22
+ const user = service.user;
23
+ const boxWidth = getLogoWidth();
24
+ const W = boxWidth - 2; // Same width as logo (inner width)
25
+
26
+ // Helper to center text
27
+ const centerLine = (text, width) => {
28
+ const pad = Math.floor((width - text.length) / 2);
29
+ return ' '.repeat(Math.max(0, pad)) + text + ' '.repeat(Math.max(0, width - pad - text.length));
30
+ };
31
+
32
+ // Helper to pad text left
33
+ const padLine = (text, width) => {
34
+ return ' ' + text + ' '.repeat(Math.max(0, width - text.length - 1));
35
+ };
36
+
37
+ // Dashboard box header
38
+ console.log();
39
+ console.log(chalk.cyan('╔' + '═'.repeat(W) + '╗'));
40
+ console.log(chalk.cyan('║') + chalk.yellow.bold(centerLine('Welcome, HQX Trader!', W)) + chalk.cyan('║'));
41
+ console.log(chalk.cyan('╠' + '═'.repeat(W) + '╣'));
42
+
43
+ // Connection info - show all active connections in boxes (max 3 per row)
44
+ const allConns = connections.getAll();
45
+ if (allConns.length > 0) {
46
+ const maxPerRow = 3;
47
+ const boxPadding = 2; // padding inside each mini-box
48
+ const gap = 2; // gap between boxes
49
+
50
+ // Calculate box width based on number of connections (max 3)
51
+ const numBoxes = Math.min(allConns.length, maxPerRow);
52
+ const totalGaps = (numBoxes - 1) * gap;
53
+ const connBoxWidth = Math.floor((W - totalGaps - 2) / numBoxes); // -2 for outer padding
54
+
55
+ // Process connections in rows of 3
56
+ for (let rowStart = 0; rowStart < allConns.length; rowStart += maxPerRow) {
57
+ const rowConns = allConns.slice(rowStart, rowStart + maxPerRow);
58
+ const numInRow = rowConns.length;
59
+ const rowBoxWidth = Math.floor((W - (numInRow - 1) * gap - 2) / numInRow);
60
+
61
+ // Top border of boxes
62
+ let topLine = ' ';
63
+ for (let i = 0; i < numInRow; i++) {
64
+ topLine += '┌' + '─'.repeat(rowBoxWidth - 2) + '┐';
65
+ if (i < numInRow - 1) topLine += ' '.repeat(gap);
66
+ }
67
+ const topPad = W - topLine.length;
68
+ console.log(chalk.cyan('║') + chalk.green(topLine) + ' '.repeat(Math.max(0, topPad)) + chalk.cyan('║'));
69
+
70
+ // Content of boxes
71
+ let contentLine = ' ';
72
+ for (let i = 0; i < numInRow; i++) {
73
+ const connText = rowConns[i].propfirm || rowConns[i].type || 'Connected';
74
+ const truncated = connText.length > rowBoxWidth - 4 ? connText.slice(0, rowBoxWidth - 7) + '...' : connText;
75
+ const innerWidth = rowBoxWidth - 4; // -2 for borders, -2 for padding
76
+ const textPad = Math.floor((innerWidth - truncated.length) / 2);
77
+ const textPadRight = innerWidth - truncated.length - textPad;
78
+ contentLine += '│ ' + ' '.repeat(textPad) + truncated + ' '.repeat(textPadRight) + ' │';
79
+ if (i < numInRow - 1) contentLine += ' '.repeat(gap);
80
+ }
81
+ const contentPad = W - contentLine.length;
82
+ console.log(chalk.cyan('║') + chalk.green(contentLine) + ' '.repeat(Math.max(0, contentPad)) + chalk.cyan('║'));
83
+
84
+ // Bottom border of boxes
85
+ let bottomLine = ' ';
86
+ for (let i = 0; i < numInRow; i++) {
87
+ bottomLine += '└' + '─'.repeat(rowBoxWidth - 2) + '┘';
88
+ if (i < numInRow - 1) bottomLine += ' '.repeat(gap);
89
+ }
90
+ const bottomPad = W - bottomLine.length;
91
+ console.log(chalk.cyan('║') + chalk.green(bottomLine) + ' '.repeat(Math.max(0, bottomPad)) + chalk.cyan('║'));
92
+ }
93
+ }
94
+
95
+ console.log(chalk.cyan('╠' + '═'.repeat(W) + '╣'));
96
+
97
+ // Menu options in 2 columns
98
+ const col1Width = Math.floor(W / 2);
99
+ const col2Width = W - col1Width;
100
+
101
+ const menuRow = (left, right) => {
102
+ const leftPlain = left.replace(/\x1b\[[0-9;]*m/g, '');
103
+ const rightPlain = right ? right.replace(/\x1b\[[0-9;]*m/g, '') : '';
104
+ const leftPad = ' '.repeat(Math.max(0, col1Width - leftPlain.length - 2));
105
+ const rightPad = ' '.repeat(Math.max(0, col2Width - rightPlain.length - 2));
106
+ console.log(chalk.cyan('║') + ' ' + left + leftPad + ' ' + (right || '') + rightPad + chalk.cyan('║'));
107
+ };
108
+
109
+ menuRow(chalk.cyan('[1] View Accounts'), chalk.cyan('[2] View Stats'));
110
+ menuRow(chalk.cyan('[+] Add Prop-Account'), chalk.cyan('[A] Algo-Trading'));
111
+ menuRow(chalk.yellow('[U] Update HQX'), chalk.red('[X] Disconnect'));
112
+
113
+ console.log(chalk.cyan('╚' + '═'.repeat(W) + '╝'));
114
+ console.log();
115
+
116
+ const { action } = await inquirer.prompt([
117
+ {
118
+ type: 'input',
119
+ name: 'action',
120
+ message: chalk.cyan('Enter choice (1/2/+/A/U/X):'),
121
+ validate: (input) => {
122
+ const valid = ['1', '2', '+', 'a', 'A', 'u', 'U', 'x', 'X'];
123
+ if (valid.includes(input)) return true;
124
+ return 'Please enter a valid option';
125
+ }
126
+ }
127
+ ]);
128
+
129
+ // Map input to action
130
+ const actionMap = {
131
+ '1': 'accounts',
132
+ '2': 'stats',
133
+ '+': 'add_prop_account',
134
+ 'a': 'algotrading',
135
+ 'A': 'algotrading',
136
+ 'u': 'update',
137
+ 'U': 'update',
138
+ 'x': 'disconnect',
139
+ 'X': 'disconnect'
140
+ };
141
+
142
+ return actionMap[action] || 'accounts';
143
+ };
144
+
145
+ /**
146
+ * Wait for user to press Enter
147
+ */
148
+ const waitForEnter = async () => {
149
+ prepareStdin();
150
+ try {
151
+ await inquirer.prompt([{ type: 'input', name: 'c', message: 'Press Enter to continue...' }]);
152
+ } catch (e) {
153
+ // Ignore prompt errors
154
+ }
155
+ };
156
+
157
+ /**
158
+ * Handles the update process with auto-restart
159
+ * Robust version that handles all edge cases
160
+ */
161
+ const handleUpdate = async () => {
162
+ prepareStdin();
163
+
164
+ let spinner = null;
165
+ let currentVersion = 'unknown';
166
+ let latestVersion = null;
167
+
168
+ try {
169
+ // Get current version safely
170
+ try {
171
+ const pkg = require('../../package.json');
172
+ currentVersion = pkg.version || 'unknown';
173
+ } catch (e) {
174
+ currentVersion = 'unknown';
175
+ }
176
+
177
+ spinner = ora('Checking for updates...').start();
178
+
179
+ // Check latest version on npm with timeout
180
+ spinner.text = 'Checking npm registry...';
181
+ try {
182
+ const result = execSync('npm view hedgequantx version 2>/dev/null', {
183
+ stdio: 'pipe',
184
+ timeout: 15000, // 15 second timeout
185
+ encoding: 'utf8'
186
+ });
187
+ latestVersion = (result || '').toString().trim();
188
+
189
+ // Validate version format (x.y.z)
190
+ if (!latestVersion || !/^\d+\.\d+\.\d+/.test(latestVersion)) {
191
+ throw new Error('Invalid version format received');
192
+ }
193
+ } catch (e) {
194
+ spinner.fail('Cannot reach npm registry');
195
+ console.log(chalk.gray(' Check your internet connection'));
196
+ console.log();
197
+ await waitForEnter();
198
+ return;
199
+ }
200
+
201
+ // Compare versions
202
+ if (currentVersion === latestVersion) {
203
+ spinner.succeed('Already up to date!');
204
+ console.log();
205
+ console.log(chalk.green(` You have the latest version: v${currentVersion}`));
206
+ console.log();
207
+ await waitForEnter();
208
+ return;
209
+ }
210
+
211
+ // Ask user before updating
212
+ spinner.stop();
213
+ console.log();
214
+ console.log(chalk.cyan(` Current version: v${currentVersion}`));
215
+ console.log(chalk.green(` Latest version: v${latestVersion}`));
216
+ console.log();
217
+
218
+ prepareStdin();
219
+ const { confirm } = await inquirer.prompt([{
220
+ type: 'confirm',
221
+ name: 'confirm',
222
+ message: 'Do you want to update now?',
223
+ default: true
224
+ }]);
225
+
226
+ if (!confirm) {
227
+ console.log(chalk.gray(' Update cancelled'));
228
+ console.log();
229
+ await waitForEnter();
230
+ return;
231
+ }
232
+
233
+ // Update via npm
234
+ spinner = ora(`Updating v${currentVersion} -> v${latestVersion}...`).start();
235
+
236
+ try {
237
+ execSync('npm install -g hedgequantx@latest 2>/dev/null', {
238
+ stdio: 'pipe',
239
+ timeout: 120000, // 2 minute timeout for install
240
+ encoding: 'utf8'
241
+ });
242
+ } catch (e) {
243
+ spinner.fail('Update failed');
244
+ console.log();
245
+ console.log(chalk.yellow(' Try manually:'));
246
+ console.log(chalk.white(' npm install -g hedgequantx@latest'));
247
+ console.log();
248
+ if (e.message) {
249
+ console.log(chalk.gray(` Error: ${e.message.substring(0, 100)}`));
250
+ console.log();
251
+ }
252
+ await waitForEnter();
253
+ return;
254
+ }
255
+
256
+ spinner.succeed('Update complete!');
257
+ console.log();
258
+ console.log(chalk.green(` Updated: v${currentVersion} -> v${latestVersion}`));
259
+ console.log();
260
+
261
+ // Ask if user wants to restart
262
+ prepareStdin();
263
+ const { restart } = await inquirer.prompt([{
264
+ type: 'confirm',
265
+ name: 'restart',
266
+ message: 'Restart HQX now?',
267
+ default: true
268
+ }]);
269
+
270
+ if (restart) {
271
+ console.log();
272
+ console.log(chalk.cyan(' Restarting HedgeQuantX CLI...'));
273
+ console.log();
274
+
275
+ // Small delay so user can see the message
276
+ await new Promise(resolve => setTimeout(resolve, 1000));
277
+
278
+ // Restart the CLI
279
+ try {
280
+ const child = spawn('hedgequantx', [], {
281
+ stdio: 'inherit',
282
+ detached: true,
283
+ shell: true
284
+ });
285
+ child.unref();
286
+ process.exit(0);
287
+ } catch (e) {
288
+ console.log(chalk.yellow(' Could not auto-restart. Please run: hedgequantx'));
289
+ console.log();
290
+ await waitForEnter();
291
+ }
292
+ } else {
293
+ console.log(chalk.gray(' Run "hedgequantx" to use the new version'));
294
+ console.log();
295
+ await waitForEnter();
296
+ }
297
+
298
+ } catch (error) {
299
+ // Catch-all for any unexpected errors
300
+ if (spinner) {
301
+ try { spinner.fail('Update error'); } catch (e) {}
302
+ }
303
+ console.log();
304
+ console.log(chalk.red(' An error occurred during update'));
305
+ if (error && error.message) {
306
+ console.log(chalk.gray(` ${error.message.substring(0, 100)}`));
307
+ }
308
+ console.log();
309
+ console.log(chalk.yellow(' Try manually: npm install -g hedgequantx@latest'));
310
+ console.log();
311
+ await waitForEnter();
312
+ }
313
+ };
314
+
315
+ module.exports = {
316
+ dashboardMenu,
317
+ handleUpdate
318
+ };
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Menus - Export all menu modules
3
+ */
4
+
5
+ module.exports = {
6
+ ...require('./connect'),
7
+ ...require('./dashboard')
8
+ };
@@ -193,8 +193,9 @@ class AlgoUI {
193
193
  this._line(chalk.cyan(BOX.V) + chalk.white(left) + ' '.repeat(midPad) + chalk.cyan(mid) + ' '.repeat(space - midPad - mid.length) + chalk.yellow(right) + chalk.cyan(BOX.V));
194
194
  this._line(chalk.cyan(BOX.ML + BOX.H.repeat(W) + BOX.MR));
195
195
 
196
- // Logs (newest first)
197
- const visible = [...logs].reverse().slice(0, maxLogs);
196
+ // Logs (oldest at top, newest at bottom - like a terminal)
197
+ // Take the last maxLogs entries (most recent), keep chronological order
198
+ const visible = logs.slice(-maxLogs);
198
199
 
199
200
  if (visible.length === 0) {
200
201
  this._line(chalk.cyan(BOX.V) + chalk.gray(fitToWidth(' Waiting for activity...', W)) + chalk.cyan(BOX.V));
@@ -202,15 +203,17 @@ class AlgoUI {
202
203
  this._line(chalk.cyan(BOX.V) + ' '.repeat(W) + chalk.cyan(BOX.V));
203
204
  }
204
205
  } else {
206
+ // First draw empty lines for padding (so logs stick to bottom)
207
+ for (let i = visible.length; i < maxLogs; i++) {
208
+ this._line(chalk.cyan(BOX.V) + ' '.repeat(W) + chalk.cyan(BOX.V));
209
+ }
210
+ // Then draw logs (oldest first, newest last/at bottom)
205
211
  visible.forEach(log => {
206
212
  const color = LOG_COLORS[log.type] || chalk.white;
207
213
  const icon = LOG_ICONS[log.type] || LOG_ICONS.info;
208
214
  const line = ` [${log.timestamp}] ${icon} ${log.message}`;
209
215
  this._line(chalk.cyan(BOX.V) + color(fitToWidth(line, W)) + chalk.cyan(BOX.V));
210
216
  });
211
- for (let i = visible.length; i < maxLogs; i++) {
212
- this._line(chalk.cyan(BOX.V) + ' '.repeat(W) + chalk.cyan(BOX.V));
213
- }
214
217
  }
215
218
 
216
219
  // Bottom border
package/src/ui/index.js CHANGED
@@ -24,6 +24,26 @@ const {
24
24
  } = require('./table');
25
25
  const { createBoxMenu } = require('./menu');
26
26
 
27
+ /**
28
+ * Ensure stdin is ready for inquirer prompts
29
+ * This fixes input leaking to bash after session restore or algo trading
30
+ */
31
+ const prepareStdin = () => {
32
+ try {
33
+ // Remove any raw mode that might be left from previous operations
34
+ if (process.stdin.isTTY && process.stdin.isRaw) {
35
+ process.stdin.setRawMode(false);
36
+ }
37
+ // Remove any lingering keypress listeners
38
+ process.stdin.removeAllListeners('keypress');
39
+ process.stdin.removeAllListeners('data');
40
+ // Pause stdin so inquirer can take control
41
+ process.stdin.pause();
42
+ } catch (e) {
43
+ // Ignore errors
44
+ }
45
+ };
46
+
27
47
  module.exports = {
28
48
  // Device
29
49
  detectDevice,
@@ -47,5 +67,7 @@ module.exports = {
47
67
  draw2ColSeparator,
48
68
  fmtRow,
49
69
  // Menu
50
- createBoxMenu
70
+ createBoxMenu,
71
+ // Stdin
72
+ prepareStdin
51
73
  };