hedgequantx 2.9.218 → 2.9.220
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/package.json +1 -1
- package/src/app.js +163 -83
- package/src/menus/connect.js +65 -1
- package/src/menus/dashboard.js +21 -20
package/package.json
CHANGED
package/src/app.js
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @fileoverview Main application router - Rithmic Only
|
|
2
|
+
* @fileoverview Main application router - Rithmic Only (Daemon Mode)
|
|
3
3
|
* @module app
|
|
4
|
+
*
|
|
5
|
+
* The TUI always uses the daemon for Rithmic connections.
|
|
6
|
+
* Daemon is auto-started in background if not running.
|
|
7
|
+
* This allows TUI updates without losing connection.
|
|
4
8
|
*/
|
|
5
9
|
|
|
6
10
|
const chalk = require('chalk');
|
|
@@ -10,6 +14,7 @@ const { connections } = require('./services');
|
|
|
10
14
|
const { getLogoWidth, centerText, prepareStdin, clearScreen } = require('./ui');
|
|
11
15
|
const { logger, prompts } = require('./utils');
|
|
12
16
|
const { setCachedStats, clearCachedStats } = require('./services/stats-cache');
|
|
17
|
+
const { startDaemonBackground, isDaemonRunning, getDaemonClient } = require('./services/daemon');
|
|
13
18
|
|
|
14
19
|
const log = logger.scope('App');
|
|
15
20
|
|
|
@@ -22,10 +27,55 @@ const { aiAgentsMenu, getActiveAgentCount } = require('./pages/ai-agents');
|
|
|
22
27
|
// Menus
|
|
23
28
|
const { rithmicMenu, dashboardMenu, handleUpdate } = require('./menus');
|
|
24
29
|
const { PROPFIRM_CHOICES } = require('./config');
|
|
30
|
+
const { showPropfirmSelection } = require('./menus/connect');
|
|
25
31
|
|
|
26
32
|
/** @type {Object|null} */
|
|
27
33
|
let currentService = null;
|
|
28
34
|
|
|
35
|
+
/** @type {Object|null} Daemon client for IPC */
|
|
36
|
+
let daemonClient = null;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Create a proxy service that uses daemon for all operations
|
|
40
|
+
* @param {Object} client - DaemonClient instance
|
|
41
|
+
* @param {Object} propfirm - Propfirm info
|
|
42
|
+
* @returns {Object} Service-like object
|
|
43
|
+
*/
|
|
44
|
+
function createDaemonProxyService(client, propfirm) {
|
|
45
|
+
const checkMarketHours = () => {
|
|
46
|
+
const now = new Date(), utcDay = now.getUTCDay(), utcHour = now.getUTCHours();
|
|
47
|
+
const isDST = now.getTimezoneOffset() < Math.max(
|
|
48
|
+
new Date(now.getFullYear(), 0, 1).getTimezoneOffset(),
|
|
49
|
+
new Date(now.getFullYear(), 6, 1).getTimezoneOffset());
|
|
50
|
+
const ctOffset = isDST ? 5 : 6, ctHour = (utcHour - ctOffset + 24) % 24;
|
|
51
|
+
const ctDay = utcHour < ctOffset ? (utcDay + 6) % 7 : utcDay;
|
|
52
|
+
if (ctDay === 6) return { isOpen: false, message: 'Market closed (Saturday)' };
|
|
53
|
+
if (ctDay === 0 && ctHour < 17) return { isOpen: false, message: 'Market opens Sunday 5PM CT' };
|
|
54
|
+
if (ctDay === 5 && ctHour >= 16) return { isOpen: false, message: 'Market closed (Friday 4PM CT)' };
|
|
55
|
+
if (ctHour === 16 && ctDay >= 1 && ctDay <= 4) return { isOpen: false, message: 'Daily maintenance' };
|
|
56
|
+
return { isOpen: true, message: 'Market is open' };
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
propfirm, propfirmKey: propfirm?.key, accounts: [], credentials: null,
|
|
61
|
+
async getTradingAccounts() { return client.getTradingAccounts(); },
|
|
62
|
+
async getPositions() { return client.getPositions(); },
|
|
63
|
+
async getOrders() { return client.getOrders(); },
|
|
64
|
+
async placeOrder(data) { return client.placeOrder(data); },
|
|
65
|
+
async cancelOrder(orderId) { return client.cancelOrder(orderId); },
|
|
66
|
+
async cancelAllOrders(accountId) { return client.cancelAllOrders(accountId); },
|
|
67
|
+
async closePosition(accountId, symbol) { return client.closePosition(accountId, symbol); },
|
|
68
|
+
async getContracts() { return client.getContracts(); },
|
|
69
|
+
async searchContracts(search) { return client.searchContracts(search); },
|
|
70
|
+
getAccountPnL() { return { pnl: null, openPnl: null, closedPnl: null, balance: null }; },
|
|
71
|
+
getToken() { return 'daemon-connected'; },
|
|
72
|
+
getPropfirm() { return propfirm?.key || 'apex'; },
|
|
73
|
+
getRithmicCredentials() { return null; },
|
|
74
|
+
checkMarketHours,
|
|
75
|
+
async disconnect() { return { success: true }; },
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
29
79
|
// ==================== TERMINAL ====================
|
|
30
80
|
|
|
31
81
|
const restoreTerminal = () => {
|
|
@@ -176,20 +226,92 @@ const run = async () => {
|
|
|
176
226
|
try {
|
|
177
227
|
log.info('Starting HQX CLI');
|
|
178
228
|
|
|
179
|
-
// First launch - show banner
|
|
229
|
+
// First launch - show banner
|
|
180
230
|
await banner();
|
|
181
231
|
|
|
182
|
-
|
|
232
|
+
// ==================== DAEMON AUTO-START ====================
|
|
233
|
+
// Always ensure daemon is running for persistent connections
|
|
234
|
+
let spinner = ora({ text: 'Starting daemon...', color: 'cyan' }).start();
|
|
235
|
+
|
|
236
|
+
if (!isDaemonRunning()) {
|
|
237
|
+
const daemonStarted = await startDaemonBackground();
|
|
238
|
+
if (!daemonStarted) {
|
|
239
|
+
spinner.warn('Daemon failed to start - using direct mode');
|
|
240
|
+
await new Promise(r => setTimeout(r, 500));
|
|
241
|
+
} else {
|
|
242
|
+
spinner.succeed('Daemon started');
|
|
243
|
+
}
|
|
244
|
+
} else {
|
|
245
|
+
spinner.succeed('Daemon running');
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Connect to daemon
|
|
249
|
+
daemonClient = getDaemonClient();
|
|
250
|
+
const daemonConnected = await daemonClient.connect();
|
|
251
|
+
|
|
252
|
+
if (!daemonConnected) {
|
|
253
|
+
log.warn('Could not connect to daemon, falling back to direct mode');
|
|
254
|
+
}
|
|
183
255
|
|
|
184
|
-
|
|
256
|
+
// ==================== SESSION RESTORE ====================
|
|
257
|
+
spinner = ora({ text: 'Restoring session...', color: 'cyan' }).start();
|
|
258
|
+
|
|
259
|
+
// Try to restore via daemon first
|
|
260
|
+
let restored = false;
|
|
261
|
+
if (daemonConnected) {
|
|
262
|
+
try {
|
|
263
|
+
const status = await daemonClient.getStatus();
|
|
264
|
+
|
|
265
|
+
if (status.connected) {
|
|
266
|
+
// Daemon already has a connection, use it
|
|
267
|
+
const accountsResult = await daemonClient.getTradingAccounts();
|
|
268
|
+
if (accountsResult.success && accountsResult.accounts?.length > 0) {
|
|
269
|
+
// Create a proxy service that uses daemon
|
|
270
|
+
currentService = createDaemonProxyService(daemonClient, status.propfirm);
|
|
271
|
+
connections.services.push({
|
|
272
|
+
type: 'rithmic',
|
|
273
|
+
service: currentService,
|
|
274
|
+
propfirm: status.propfirm?.name,
|
|
275
|
+
propfirmKey: status.propfirm?.key,
|
|
276
|
+
connectedAt: new Date(),
|
|
277
|
+
});
|
|
278
|
+
restored = true;
|
|
279
|
+
spinner.succeed(`Session active: ${status.propfirm?.name} (${accountsResult.accounts.length} accounts)`);
|
|
280
|
+
}
|
|
281
|
+
} else {
|
|
282
|
+
// Daemon not connected, try to restore session via daemon
|
|
283
|
+
const restoreResult = await daemonClient.restoreSession();
|
|
284
|
+
if (restoreResult.success) {
|
|
285
|
+
currentService = createDaemonProxyService(daemonClient, restoreResult.propfirm);
|
|
286
|
+
connections.services.push({
|
|
287
|
+
type: 'rithmic',
|
|
288
|
+
service: currentService,
|
|
289
|
+
propfirm: restoreResult.propfirm?.name,
|
|
290
|
+
propfirmKey: restoreResult.propfirm?.key,
|
|
291
|
+
connectedAt: new Date(),
|
|
292
|
+
});
|
|
293
|
+
restored = true;
|
|
294
|
+
spinner.succeed(`Session restored: ${restoreResult.propfirm?.name} (${restoreResult.accounts?.length || 0} accounts)`);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
} catch (err) {
|
|
298
|
+
log.warn('Daemon restore failed', { error: err.message });
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// Fallback to direct restore if daemon failed
|
|
303
|
+
if (!restored) {
|
|
304
|
+
restored = await connections.restoreFromStorage();
|
|
305
|
+
if (restored) {
|
|
306
|
+
const conn = connections.getAll()[0];
|
|
307
|
+
currentService = conn.service;
|
|
308
|
+
const accountCount = currentService.accounts?.length || 0;
|
|
309
|
+
spinner.succeed(`Session restored: ${conn.propfirm} (${accountCount} accounts)`);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
185
312
|
|
|
186
313
|
if (restored) {
|
|
187
|
-
const conn = connections.getAll()[0];
|
|
188
|
-
currentService = conn.service;
|
|
189
|
-
const accountCount = currentService.accounts?.length || 0;
|
|
190
|
-
spinner.succeed(`Session restored: ${conn.propfirm} (${accountCount} accounts)`);
|
|
191
314
|
await new Promise(r => setTimeout(r, 500));
|
|
192
|
-
|
|
193
315
|
const spinner2 = ora({ text: 'Loading dashboard...', color: 'yellow' }).start();
|
|
194
316
|
await refreshStats();
|
|
195
317
|
global.__hqxSpinner = spinner2;
|
|
@@ -207,84 +329,42 @@ const run = async () => {
|
|
|
207
329
|
if (!connections.isConnected()) {
|
|
208
330
|
// Not connected - show banner + propfirm selection
|
|
209
331
|
await banner();
|
|
210
|
-
// Not connected - show propfirm selection directly
|
|
211
|
-
const boxWidth = getLogoWidth();
|
|
212
|
-
const innerWidth = boxWidth - 2;
|
|
213
|
-
const numCols = 3;
|
|
214
332
|
|
|
215
|
-
const
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
// Find max name length for alignment
|
|
219
|
-
const maxNameLen = Math.max(...numbered.map(n => n.name.length));
|
|
220
|
-
const itemWidth = 4 + 1 + maxNameLen; // [##] + space + name
|
|
221
|
-
const gap = 3; // gap between columns
|
|
222
|
-
const totalContentWidth = (itemWidth * numCols) + (gap * (numCols - 1));
|
|
223
|
-
|
|
224
|
-
// New rectangle (banner is always closed)
|
|
225
|
-
console.log(chalk.cyan('╔' + '═'.repeat(innerWidth) + '╗'));
|
|
226
|
-
console.log(chalk.cyan('║') + chalk.white.bold(centerText('SELECT PROPFIRM', innerWidth)) + chalk.cyan('║'));
|
|
227
|
-
console.log(chalk.cyan('╠' + '═'.repeat(innerWidth) + '╣'));
|
|
228
|
-
|
|
229
|
-
const rows = Math.ceil(numbered.length / numCols);
|
|
230
|
-
for (let row = 0; row < rows; row++) {
|
|
231
|
-
let lineParts = [];
|
|
232
|
-
for (let col = 0; col < numCols; col++) {
|
|
233
|
-
const idx = row + col * rows;
|
|
234
|
-
if (idx < numbered.length) {
|
|
235
|
-
const item = numbered[idx];
|
|
236
|
-
const numStr = item.num.toString().padStart(2, ' ');
|
|
237
|
-
const namePadded = item.name.padEnd(maxNameLen);
|
|
238
|
-
lineParts.push({ num: `[${numStr}]`, name: namePadded });
|
|
239
|
-
} else {
|
|
240
|
-
lineParts.push(null);
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
// Build line content
|
|
245
|
-
let content = '';
|
|
246
|
-
for (let i = 0; i < lineParts.length; i++) {
|
|
247
|
-
if (lineParts[i]) {
|
|
248
|
-
content += chalk.cyan(lineParts[i].num) + ' ' + chalk.white(lineParts[i].name);
|
|
249
|
-
} else {
|
|
250
|
-
content += ' '.repeat(itemWidth);
|
|
251
|
-
}
|
|
252
|
-
if (i < lineParts.length - 1) content += ' '.repeat(gap);
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
// Center the content
|
|
256
|
-
const contentLen = content.replace(/\x1b\[[0-9;]*m/g, '').length;
|
|
257
|
-
const leftPad = Math.floor((innerWidth - contentLen) / 2);
|
|
258
|
-
const rightPad = innerWidth - contentLen - leftPad;
|
|
259
|
-
console.log(chalk.cyan('║') + ' '.repeat(leftPad) + content + ' '.repeat(rightPad) + chalk.cyan('║'));
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
console.log(chalk.cyan('╠' + '─'.repeat(innerWidth) + '╣'));
|
|
263
|
-
console.log(chalk.cyan('║') + chalk.red(centerText('[X] EXIT', innerWidth)) + chalk.cyan('║'));
|
|
264
|
-
console.log(chalk.cyan('╚' + '═'.repeat(innerWidth) + '╝'));
|
|
265
|
-
|
|
266
|
-
const input = await prompts.textInput(chalk.cyan('SELECT (1-' + numbered.length + '/X): '));
|
|
267
|
-
|
|
268
|
-
if (!input || input.toLowerCase() === 'x') {
|
|
333
|
+
const selectedPropfirm = await showPropfirmSelection();
|
|
334
|
+
if (!selectedPropfirm) {
|
|
269
335
|
console.log(chalk.gray('GOODBYE!'));
|
|
270
336
|
process.exit(0);
|
|
271
337
|
}
|
|
272
338
|
|
|
273
|
-
const
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
const
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
339
|
+
const { loginPrompt } = require('./menus/connect');
|
|
340
|
+
const credentials = await loginPrompt(selectedPropfirm.name);
|
|
341
|
+
|
|
342
|
+
if (credentials) {
|
|
343
|
+
const spinner = ora({ text: 'CONNECTING TO RITHMIC...', color: 'yellow' }).start();
|
|
344
|
+
try {
|
|
345
|
+
let result;
|
|
346
|
+
|
|
347
|
+
// Try daemon connection first (persistent)
|
|
348
|
+
if (daemonClient?.connected) {
|
|
349
|
+
result = await daemonClient.login(selectedPropfirm.key, credentials.username, credentials.password);
|
|
350
|
+
if (result.success) {
|
|
351
|
+
currentService = createDaemonProxyService(daemonClient, result.propfirm);
|
|
352
|
+
connections.services.push({
|
|
353
|
+
type: 'rithmic', service: currentService,
|
|
354
|
+
propfirm: selectedPropfirm.name, propfirmKey: selectedPropfirm.key, connectedAt: new Date(),
|
|
355
|
+
});
|
|
356
|
+
spinner.succeed(`CONNECTED TO ${selectedPropfirm.name.toUpperCase()} (${result.accounts?.length || 0} ACCOUNTS) [DAEMON]`);
|
|
357
|
+
await refreshStats();
|
|
358
|
+
await new Promise(r => setTimeout(r, 1500));
|
|
359
|
+
} else {
|
|
360
|
+
spinner.fail((result.error || 'AUTHENTICATION FAILED').toUpperCase());
|
|
361
|
+
await new Promise(r => setTimeout(r, 2000));
|
|
362
|
+
}
|
|
363
|
+
} else {
|
|
364
|
+
// Fallback to direct connection
|
|
283
365
|
const { RithmicService } = require('./services/rithmic');
|
|
284
|
-
|
|
285
366
|
const service = new RithmicService(selectedPropfirm.key);
|
|
286
|
-
|
|
287
|
-
|
|
367
|
+
result = await service.login(credentials.username, credentials.password);
|
|
288
368
|
if (result.success) {
|
|
289
369
|
connections.add('rithmic', service, selectedPropfirm.name);
|
|
290
370
|
spinner.succeed(`CONNECTED TO ${selectedPropfirm.name.toUpperCase()} (${result.accounts?.length || 0} ACCOUNTS)`);
|
|
@@ -295,10 +375,10 @@ const run = async () => {
|
|
|
295
375
|
spinner.fail((result.error || 'AUTHENTICATION FAILED').toUpperCase());
|
|
296
376
|
await new Promise(r => setTimeout(r, 2000));
|
|
297
377
|
}
|
|
298
|
-
} catch (error) {
|
|
299
|
-
spinner.fail(`CONNECTION ERROR: ${error.message.toUpperCase()}`);
|
|
300
|
-
await new Promise(r => setTimeout(r, 2000));
|
|
301
378
|
}
|
|
379
|
+
} catch (error) {
|
|
380
|
+
spinner.fail(`CONNECTION ERROR: ${error.message.toUpperCase()}`);
|
|
381
|
+
await new Promise(r => setTimeout(r, 2000));
|
|
302
382
|
}
|
|
303
383
|
}
|
|
304
384
|
} else {
|
package/src/menus/connect.js
CHANGED
|
@@ -147,4 +147,68 @@ const rithmicMenu = async () => {
|
|
|
147
147
|
}
|
|
148
148
|
};
|
|
149
149
|
|
|
150
|
-
|
|
150
|
+
/**
|
|
151
|
+
* Show propfirm selection menu and return selected propfirm
|
|
152
|
+
* @returns {Promise<{key: string, name: string}|null>}
|
|
153
|
+
*/
|
|
154
|
+
const showPropfirmSelection = async () => {
|
|
155
|
+
const boxWidth = getLogoWidth();
|
|
156
|
+
const innerWidth = boxWidth - 2;
|
|
157
|
+
const numCols = 3;
|
|
158
|
+
|
|
159
|
+
const propfirms = PROPFIRM_CHOICES;
|
|
160
|
+
const numbered = propfirms.map((pf, i) => ({ num: i + 1, key: pf.value, name: pf.name }));
|
|
161
|
+
const maxNameLen = Math.max(...numbered.map(n => n.name.length));
|
|
162
|
+
const itemWidth = 4 + 1 + maxNameLen;
|
|
163
|
+
const gap = 3;
|
|
164
|
+
|
|
165
|
+
console.log(chalk.cyan('╔' + '═'.repeat(innerWidth) + '╗'));
|
|
166
|
+
console.log(chalk.cyan('║') + chalk.white.bold(centerText('SELECT PROPFIRM', innerWidth)) + chalk.cyan('║'));
|
|
167
|
+
console.log(chalk.cyan('╠' + '═'.repeat(innerWidth) + '╣'));
|
|
168
|
+
|
|
169
|
+
const rows = Math.ceil(numbered.length / numCols);
|
|
170
|
+
for (let row = 0; row < rows; row++) {
|
|
171
|
+
let lineParts = [];
|
|
172
|
+
for (let col = 0; col < numCols; col++) {
|
|
173
|
+
const idx = row + col * rows;
|
|
174
|
+
if (idx < numbered.length) {
|
|
175
|
+
const item = numbered[idx];
|
|
176
|
+
const numStr = item.num.toString().padStart(2, ' ');
|
|
177
|
+
const namePadded = item.name.padEnd(maxNameLen);
|
|
178
|
+
lineParts.push({ num: `[${numStr}]`, name: namePadded });
|
|
179
|
+
} else {
|
|
180
|
+
lineParts.push(null);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
let content = '';
|
|
185
|
+
for (let i = 0; i < lineParts.length; i++) {
|
|
186
|
+
if (lineParts[i]) {
|
|
187
|
+
content += chalk.cyan(lineParts[i].num) + ' ' + chalk.white(lineParts[i].name);
|
|
188
|
+
} else {
|
|
189
|
+
content += ' '.repeat(itemWidth);
|
|
190
|
+
}
|
|
191
|
+
if (i < lineParts.length - 1) content += ' '.repeat(gap);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const contentLen = content.replace(/\x1b\[[0-9;]*m/g, '').length;
|
|
195
|
+
const leftPad = Math.floor((innerWidth - contentLen) / 2);
|
|
196
|
+
const rightPad = innerWidth - contentLen - leftPad;
|
|
197
|
+
console.log(chalk.cyan('║') + ' '.repeat(leftPad) + content + ' '.repeat(rightPad) + chalk.cyan('║'));
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
console.log(chalk.cyan('╠' + '─'.repeat(innerWidth) + '╣'));
|
|
201
|
+
console.log(chalk.cyan('║') + chalk.red(centerText('[X] EXIT', innerWidth)) + chalk.cyan('║'));
|
|
202
|
+
console.log(chalk.cyan('╚' + '═'.repeat(innerWidth) + '╝'));
|
|
203
|
+
|
|
204
|
+
const input = await prompts.textInput(chalk.cyan('SELECT (1-' + numbered.length + '/X): '));
|
|
205
|
+
|
|
206
|
+
if (!input || input.toLowerCase() === 'x') return null;
|
|
207
|
+
|
|
208
|
+
const action = parseInt(input);
|
|
209
|
+
if (isNaN(action) || action < 1 || action > numbered.length) return null;
|
|
210
|
+
|
|
211
|
+
return numbered[action - 1];
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
module.exports = { loginPrompt, rithmicMenu, showPropfirmSelection };
|
package/src/menus/dashboard.js
CHANGED
|
@@ -71,30 +71,31 @@ const dashboardMenu = async (service) => {
|
|
|
71
71
|
// Daemon status
|
|
72
72
|
const daemonOn = isDaemonRunning();
|
|
73
73
|
const daemonDisplay = daemonOn ? 'ON' : 'OFF';
|
|
74
|
-
const daemonColor = daemonOn ? chalk.green : chalk.
|
|
74
|
+
const daemonColor = daemonOn ? chalk.green : chalk.red;
|
|
75
75
|
|
|
76
|
-
//
|
|
77
|
-
const icon = chalk.yellow('✔
|
|
78
|
-
const
|
|
76
|
+
// Build stats items
|
|
77
|
+
const icon = chalk.yellow('✔');
|
|
78
|
+
const items = [
|
|
79
|
+
{ label: 'Accounts', value: String(statsInfo.accounts), color: chalk.white },
|
|
80
|
+
{ label: 'Balance', value: balStr, color: balColor },
|
|
81
|
+
{ label: 'Daemon', value: daemonDisplay, color: daemonColor },
|
|
82
|
+
{ label: 'AI', value: agentDisplay, color: agentColor },
|
|
83
|
+
];
|
|
79
84
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
const padRight = colWidth - textLen - padLeft;
|
|
85
|
-
return ' '.repeat(Math.max(0, padLeft)) + icon + chalk.white(label + ': ') + valueColor(value) + ' '.repeat(Math.max(0, padRight));
|
|
86
|
-
};
|
|
87
|
-
|
|
88
|
-
const col1 = formatCol('Accounts', String(statsInfo.accounts));
|
|
89
|
-
const col2 = formatCol('Balance', balStr, balColor);
|
|
90
|
-
const col3 = formatCol('Daemon', daemonDisplay, daemonColor);
|
|
91
|
-
const col4 = formatCol('AI', agentDisplay, agentColor);
|
|
85
|
+
// Format: "✔ Label: Value" with consistent spacing
|
|
86
|
+
const formatted = items.map(item =>
|
|
87
|
+
`${icon} ${chalk.white(item.label + ':')} ${item.color(item.value)}`
|
|
88
|
+
);
|
|
92
89
|
|
|
93
|
-
|
|
94
|
-
const
|
|
95
|
-
const
|
|
90
|
+
// Join with consistent gaps and center the whole line
|
|
91
|
+
const gap = ' '; // 4 spaces between items
|
|
92
|
+
const statsContent = formatted.join(gap);
|
|
93
|
+
const statsPlainLen = statsContent.replace(/\x1b\[[0-9;]*m/g, '').length;
|
|
94
|
+
const totalPad = W - statsPlainLen;
|
|
95
|
+
const padLeft = Math.floor(totalPad / 2);
|
|
96
|
+
const padRight = totalPad - padLeft;
|
|
96
97
|
|
|
97
|
-
console.log(chalk.cyan('║') +
|
|
98
|
+
console.log(chalk.cyan('║') + ' '.repeat(padLeft) + statsContent + ' '.repeat(padRight) + chalk.cyan('║'));
|
|
98
99
|
}
|
|
99
100
|
|
|
100
101
|
console.log(chalk.cyan('╠' + '═'.repeat(W) + '╣'));
|