dankgrinder 7.77.0 → 7.79.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/lib/grinder.js +68 -45
- package/lib/terminal.js +739 -0
- package/package.json +1 -1
package/lib/grinder.js
CHANGED
|
@@ -3,6 +3,7 @@ const Redis = require('ioredis');
|
|
|
3
3
|
const commands = require('./commands');
|
|
4
4
|
const { setDashboardActive, isCV2, ensureCV2, stripAnsi } = require('./commands/utils');
|
|
5
5
|
const rawLogger = require('./rawLogger');
|
|
6
|
+
const terminal = require('./terminal');
|
|
6
7
|
const {
|
|
7
8
|
BloomFilter, RingBuffer, TokenBucket, EMA, SlidingWindowCounter,
|
|
8
9
|
AhoCorasick, LRUCache, StringPool, AsyncBatchQueue, JitterBackoff,
|
|
@@ -338,6 +339,17 @@ function colorBanner() {
|
|
|
338
339
|
|
|
339
340
|
// ── Simple Logging ─────────────────────────────────────────────
|
|
340
341
|
function log(type, msg, label) {
|
|
342
|
+
// Route grinding logs through terminal flash events when active
|
|
343
|
+
if (terminal._active) {
|
|
344
|
+
const clean = stripAnsi(String(msg || '')).substring(0, 120);
|
|
345
|
+
const tagClean = stripAnsi(String(label || ''));
|
|
346
|
+
terminal.flashEvent(
|
|
347
|
+
type === 'error' ? 'death' : type === 'warn' ? 'warn' : 'info',
|
|
348
|
+
`${tagClean ? tagClean + ' ' : ''}${clean}`
|
|
349
|
+
);
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
|
|
341
353
|
const colorIcons = {
|
|
342
354
|
info: `${c.dim}·${c.reset}`, success: `${rgb(52, 211, 153)}✓${c.reset}`,
|
|
343
355
|
error: `${rgb(239, 68, 68)}✗${c.reset}`, warn: `${rgb(251, 191, 36)}!${c.reset}`,
|
|
@@ -1623,6 +1635,8 @@ class AccountWorker {
|
|
|
1623
1635
|
// ── Death / lifesaver detection in command responses ──
|
|
1624
1636
|
if (resultLower.includes('you died') || resultLower.includes('lifesaver protected')) {
|
|
1625
1637
|
this.log('error', `DEATH DETECTED during ${cmdName}!`);
|
|
1638
|
+
terminal.flashEvent('death', `💀 ${this.username} died — ${lsCount === 0 ? '0 lifesavers!' : (lsCount > 0 ? `${lsCount} lifesavers left` : 'check DM')}`);
|
|
1639
|
+
terminal.markWorkerDirty(this.idx);
|
|
1626
1640
|
// Check for lifesaver count in the response
|
|
1627
1641
|
const lsMatch = result.match(/(\d+)\s*life\s*saver/i);
|
|
1628
1642
|
const lsCount = lsMatch ? parseInt(lsMatch[1]) : -1;
|
|
@@ -1639,6 +1653,8 @@ class AccountWorker {
|
|
|
1639
1653
|
await redis.set(`dkg:lifesavers:${this.account.id}`, String(lsCount), 'EX', 86400);
|
|
1640
1654
|
this.log('warn', `Lifesaver used! ${lsCount} remaining.`);
|
|
1641
1655
|
if (lsCount <= 2) {
|
|
1656
|
+
terminal.flashEvent('lowls', `⚠ ${this.username} low lifesavers: ${lsCount} left`);
|
|
1657
|
+
terminal.markWorkerDirty(this.idx);
|
|
1642
1658
|
sendWebhook('LOW LIFESAVERS', `**${this.username}** has only **${lsCount}** lifesaver(s) left!`, 0xfbbf24);
|
|
1643
1659
|
}
|
|
1644
1660
|
}
|
|
@@ -1758,6 +1774,8 @@ class AccountWorker {
|
|
|
1758
1774
|
this.setStatus(formattedResult);
|
|
1759
1775
|
await sendLog(this.username, cmdName, result, 'success');
|
|
1760
1776
|
reportEarnings(this.account.id, this.username, earned, spent, cmdName);
|
|
1777
|
+
terminal.markWorkerDirty(this.idx);
|
|
1778
|
+
if (earned > 0) terminal.flashEvent('success', `⚔ ${this.username} ${cmdName}: +⏣ ${earned.toLocaleString()}`);
|
|
1761
1779
|
|
|
1762
1780
|
// Auto-sell fish every 5 fishing rounds
|
|
1763
1781
|
if (cmdName === 'fish') {
|
|
@@ -2844,6 +2862,8 @@ async function start(apiKey, apiUrl, opts = {}) {
|
|
|
2844
2862
|
w.log?.('error', `DEATH in DMs! 0 lifesavers — disabling crime/search`);
|
|
2845
2863
|
w.setCooldown?.('crime', 86400);
|
|
2846
2864
|
w.setCooldown?.('search', 86400);
|
|
2865
|
+
terminal.flashEvent('death', `💀 ${w.username} DEATH in DM — 0 lifesavers!`);
|
|
2866
|
+
terminal.markWorkerDirty(w.idx);
|
|
2847
2867
|
sendWebhook?.('DEATH ALERT (DM)', `**${w.username}** died in DMs! **0 lifesavers!**\nCrime/search auto-disabled.`, 0xef4444);
|
|
2848
2868
|
} else {
|
|
2849
2869
|
w.log?.('warn', `DEATH in DMs — ${event.lifesaversLeft} lifesavers remaining`);
|
|
@@ -2861,6 +2881,8 @@ async function start(apiKey, apiUrl, opts = {}) {
|
|
|
2861
2881
|
if (event.type === 'levelup') {
|
|
2862
2882
|
if (event.to > 0) {
|
|
2863
2883
|
w._level = event.to;
|
|
2884
|
+
terminal.flashEvent('levelup', `⬆️ ${w.username} leveled up to Lv.${event.to}`);
|
|
2885
|
+
terminal.markWorkerDirty(w.idx);
|
|
2864
2886
|
}
|
|
2865
2887
|
}
|
|
2866
2888
|
}
|
|
@@ -2876,8 +2898,12 @@ async function start(apiKey, apiUrl, opts = {}) {
|
|
|
2876
2898
|
console.log(` ${checks.join(' ')}`);
|
|
2877
2899
|
console.log('');
|
|
2878
2900
|
|
|
2901
|
+
// ── Terminal renderer init ─────────────────────────────────────
|
|
2902
|
+
terminal.setVersion(PKG_VERSION);
|
|
2903
|
+
terminal.init({ workers, startTime });
|
|
2904
|
+
terminal.startPhase(`Connecting ${accounts.length} accounts to Discord`);
|
|
2905
|
+
|
|
2879
2906
|
// ── Phase 1: Login accounts ─────────────────────────────────────────
|
|
2880
|
-
console.log(` ${rgb(52, 211, 153)}✓${c.reset} Logging in ${accounts.length} account(s)...`);
|
|
2881
2907
|
const parsedGapMin = Number.parseInt(String(process.env.LOGIN_GAP_MIN_MS || '50'), 10);
|
|
2882
2908
|
const parsedGapMax = Number.parseInt(String(process.env.LOGIN_GAP_MAX_MS || '150'), 10);
|
|
2883
2909
|
const LOGIN_GAP_MIN_MS = Number.isFinite(parsedGapMin) && parsedGapMin >= 0 ? parsedGapMin : 50;
|
|
@@ -2894,6 +2920,7 @@ async function start(apiKey, apiUrl, opts = {}) {
|
|
|
2894
2920
|
workers.push(worker);
|
|
2895
2921
|
workerMap.set(acc.id, worker);
|
|
2896
2922
|
await worker.start();
|
|
2923
|
+
terminal.updateProgress(workers.length, accounts.length);
|
|
2897
2924
|
}));
|
|
2898
2925
|
if (i + BATCH_SIZE < accounts.length) await new Promise(r => setTimeout(r, randomLoginGap()));
|
|
2899
2926
|
hintGC();
|
|
@@ -2902,8 +2929,9 @@ async function start(apiKey, apiUrl, opts = {}) {
|
|
|
2902
2929
|
const loginDone = workers.filter(w => !w._tokenInvalid && w.channel).length;
|
|
2903
2930
|
const invalidWorkers = workers.filter(w => w._tokenInvalid);
|
|
2904
2931
|
const timedOutWorkers = workers.filter(w => !w.channel && !w._tokenInvalid);
|
|
2905
|
-
|
|
2932
|
+
terminal.endPhase(`${loginDone}/${accounts.length} accounts connected`, true);
|
|
2906
2933
|
if (invalidWorkers.length > 0) {
|
|
2934
|
+
terminal.flashEvent('error', `${invalidWorkers.length} accounts have INVALID tokens`);
|
|
2907
2935
|
log('warn', `${rgb(239, 68, 68)}${invalidWorkers.length} account(s) have INVALID tokens`);
|
|
2908
2936
|
for (const w of invalidWorkers) log('error', ` ${w.account.label || w.account.id} — token invalid or expired`);
|
|
2909
2937
|
}
|
|
@@ -2912,24 +2940,31 @@ async function start(apiKey, apiUrl, opts = {}) {
|
|
|
2912
2940
|
const activeWorkers = workers.filter(w => !w._tokenInvalid);
|
|
2913
2941
|
|
|
2914
2942
|
// ── Phase 2: Inventory check ─────────────────────────────────────
|
|
2915
|
-
|
|
2943
|
+
terminal.startPhase('Checking inventory');
|
|
2944
|
+
terminal.updateProgress(0, activeWorkers.length);
|
|
2916
2945
|
let invDone = 0, invFailed = 0;
|
|
2917
2946
|
await Promise.all(activeWorkers.map(async (w) => {
|
|
2918
2947
|
try { await w.checkInventory({ force: true, requireComplete: true, maxAttempts: 3, silent: true }); }
|
|
2919
2948
|
catch { invFailed++; return; }
|
|
2920
2949
|
invDone++;
|
|
2950
|
+
terminal.updateProgress(invDone, activeWorkers.length);
|
|
2921
2951
|
}));
|
|
2922
2952
|
|
|
2923
2953
|
if (invFailed > 0) {
|
|
2954
|
+
terminal.endPhase(`Inventory: ${invFailed} failed`, false);
|
|
2924
2955
|
console.log(` ${rgb(239, 68, 68)}✗${c.reset} Inventory: ${invFailed} failed — not starting grind loops`);
|
|
2925
2956
|
return;
|
|
2926
2957
|
}
|
|
2927
|
-
|
|
2958
|
+
terminal.endPhase(`Inventory: ${invDone} clear`);
|
|
2928
2959
|
|
|
2929
2960
|
// ── Phase 2.5: Balance check ────────────────────────────────────
|
|
2930
|
-
|
|
2961
|
+
terminal.startPhase('Checking balance');
|
|
2962
|
+
terminal.updateProgress(0, activeWorkers.length);
|
|
2963
|
+
let balDone = 0;
|
|
2931
2964
|
await Promise.all(activeWorkers.map(async (w) => {
|
|
2932
2965
|
try { await w.checkBalance(true); } catch {}
|
|
2966
|
+
balDone++;
|
|
2967
|
+
terminal.updateProgress(balDone, activeWorkers.length);
|
|
2933
2968
|
}));
|
|
2934
2969
|
|
|
2935
2970
|
let totalWallet = 0, totalBank = 0, noLifesaverAccounts = [];
|
|
@@ -2938,13 +2973,16 @@ async function start(apiKey, apiUrl, opts = {}) {
|
|
|
2938
2973
|
totalBank += w.stats?.bankBalance || 0;
|
|
2939
2974
|
if (w._lifesavers === 0) noLifesaverAccounts.push(w.username || w.account.label);
|
|
2940
2975
|
}
|
|
2941
|
-
|
|
2976
|
+
terminal.endPhase(`Balance: ⏣ ${(totalWallet + totalBank).toLocaleString()}`);
|
|
2942
2977
|
if (noLifesaverAccounts.length > 0) {
|
|
2978
|
+
terminal.flashEvent('warn', `${noLifesaverAccounts.length} accounts have 0 LIFESAVERS`);
|
|
2943
2979
|
console.log(` ${rgb(239, 68, 68)}⚠${c.reset} ${noLifesaverAccounts.length} account(s) have 0 LIFESAVERS — crime/search disabled`);
|
|
2944
2980
|
}
|
|
2945
2981
|
|
|
2946
2982
|
// ── Phase 2.75: DM history check ────────────────────────────────
|
|
2947
|
-
|
|
2983
|
+
terminal.startPhase('Checking DM history');
|
|
2984
|
+
terminal.updateProgress(0, activeWorkers.length);
|
|
2985
|
+
let dmDone = 0;
|
|
2948
2986
|
let dmDeaths = 0, dmLevelUps = 0, dmNoLs = [], dmUnknown = [];
|
|
2949
2987
|
for (const w of activeWorkers) {
|
|
2950
2988
|
try {
|
|
@@ -2956,8 +2994,11 @@ async function start(apiKey, apiUrl, opts = {}) {
|
|
|
2956
2994
|
if (dm.currentLevel > 0) w._level = dm.currentLevel;
|
|
2957
2995
|
if (dm.lifesavers >= 0) w._lifesavers = dm.lifesavers;
|
|
2958
2996
|
} catch {}
|
|
2997
|
+
dmDone++;
|
|
2998
|
+
terminal.updateProgress(dmDone, activeWorkers.length);
|
|
2959
2999
|
}
|
|
2960
3000
|
if (dmNoLs.length > 0) {
|
|
3001
|
+
terminal.flashEvent('warn', `No lifesavers: ${dmNoLs.join(', ')}`);
|
|
2961
3002
|
log('warn', `⚠ No lifesavers: ${dmNoLs.join(', ')}`);
|
|
2962
3003
|
for (const w of activeWorkers) {
|
|
2963
3004
|
if (dmNoLs.includes(w.username) && redis) {
|
|
@@ -2975,17 +3016,16 @@ async function start(apiKey, apiUrl, opts = {}) {
|
|
|
2975
3016
|
if (dmDeaths > 0) dmSummaryParts.push(`${dmDeaths} deaths`);
|
|
2976
3017
|
if (dmLevelUps > 0) dmSummaryParts.push(`${dmLevelUps} level-ups`);
|
|
2977
3018
|
if (dmUnknown.length > 0) dmSummaryParts.push(`${dmUnknown.length} pending`);
|
|
2978
|
-
|
|
3019
|
+
terminal.endPhase(`DM check: ${dmSummaryParts.length > 0 ? dmSummaryParts.join(', ') : 'clean'}`);
|
|
2979
3020
|
|
|
2980
3021
|
// ── Phase 3: Start grind loops ───────────────────────────────────
|
|
2981
|
-
|
|
2982
|
-
// Phase 3: Start all grind loops (only for valid workers)
|
|
3022
|
+
terminal.startPhase(`🚀 Launching ${activeWorkers.length} grinders!`);
|
|
2983
3023
|
for (const w of activeWorkers) {
|
|
2984
3024
|
if (!shutdownCalled) w.grindLoop();
|
|
2985
3025
|
}
|
|
2986
|
-
|
|
2987
|
-
|
|
2988
|
-
|
|
3026
|
+
terminal.endPhase(`${activeWorkers.length} grinders active`);
|
|
3027
|
+
terminal.setWorkers(workers);
|
|
3028
|
+
terminal.setActive();
|
|
2989
3029
|
|
|
2990
3030
|
// Cluster heartbeat — lets other nodes see this node is alive
|
|
2991
3031
|
if (CLUSTER_ENABLED) {
|
|
@@ -3052,35 +3092,26 @@ async function start(apiKey, apiUrl, opts = {}) {
|
|
|
3052
3092
|
sigintHandled = true;
|
|
3053
3093
|
shutdownCalled = true;
|
|
3054
3094
|
setDashboardActive(false);
|
|
3055
|
-
process.stdout.write('\x1b[?25h'); // show cursor
|
|
3056
|
-
|
|
3057
|
-
const tw = process.stdout.columns || 80;
|
|
3058
|
-
const sepBar = rgb(139, 92, 246) + c.bold + '─'.repeat(tw - 4) + c.reset;
|
|
3059
|
-
console.log('');
|
|
3060
|
-
console.log(` ${rgb(251, 191, 36)}${c.bold}Session Summary${c.reset}`);
|
|
3061
|
-
console.log(sepBar);
|
|
3062
3095
|
|
|
3063
|
-
// Collect stats
|
|
3064
|
-
let finalCoins = 0;
|
|
3065
|
-
let finalCmds = 0;
|
|
3096
|
+
// Collect stats for summary
|
|
3097
|
+
let finalCoins = 0, finalCmds = 0, totalSuccess = 0;
|
|
3066
3098
|
for (const wk of workers) {
|
|
3067
|
-
const rate = wk.stats.commands > 0 ? ((wk.stats.successes / wk.stats.commands) * 100).toFixed(0) : 0;
|
|
3068
|
-
console.log(
|
|
3069
|
-
` ${wk.color}${c.bold}${(wk.username || '?').padEnd(18)}${c.reset}` +
|
|
3070
|
-
` ${rgb(52, 211, 153)}+⏣ ${wk.stats.coins.toLocaleString().padStart(8)}${c.reset}` +
|
|
3071
|
-
` ${c.dim}${wk.stats.commands.toString().padStart(4)} cmds${c.reset}` +
|
|
3072
|
-
` ${c.dim}${rate}% success${c.reset}`
|
|
3073
|
-
);
|
|
3074
3099
|
finalCoins += wk.stats.coins || 0;
|
|
3075
3100
|
finalCmds += wk.stats.commands || 0;
|
|
3101
|
+
totalSuccess += wk.stats.successes || 0;
|
|
3076
3102
|
}
|
|
3077
|
-
console.log(sepBar);
|
|
3078
|
-
|
|
3079
3103
|
const memFinal = Math.round((process.memoryUsage?.rss?.() ?? process.memoryUsage().rss) / 1048576);
|
|
3080
|
-
const
|
|
3081
|
-
|
|
3082
|
-
|
|
3083
|
-
|
|
3104
|
+
const uptimeMs = Date.now() - startTime;
|
|
3105
|
+
|
|
3106
|
+
// Show beautiful session summary via terminal renderer
|
|
3107
|
+
terminal.shutdown({
|
|
3108
|
+
totalCoins: finalCoins,
|
|
3109
|
+
totalCmds: finalCmds,
|
|
3110
|
+
totalSuccess: finalCmds > 0 ? ((totalSuccess / finalCmds) * 100).toFixed(0) : 0,
|
|
3111
|
+
workers,
|
|
3112
|
+
uptime: uptimeMs,
|
|
3113
|
+
memMB: memFinal,
|
|
3114
|
+
});
|
|
3084
3115
|
|
|
3085
3116
|
// Release all cluster claims before stopping workers
|
|
3086
3117
|
const releasePromises = workers.map(wk => releaseClaim(wk.account.id).catch(() => {}));
|
|
@@ -3096,22 +3127,14 @@ async function start(apiKey, apiUrl, opts = {}) {
|
|
|
3096
3127
|
|
|
3097
3128
|
const totalRecoveries = workers.reduce((sum, wk) => sum + (wk._totalRecoveries || 0), 0);
|
|
3098
3129
|
const totalDisconnects = workers.reduce((sum, wk) => sum + (wk._disconnectCount || 0), 0);
|
|
3099
|
-
const totalRateLimits = workers.reduce((sum, wk) => sum + (wk._rateLimitHits || 0), 0);
|
|
3100
3130
|
|
|
3101
3131
|
const webhookMsg = `+⏣ ${finalCoins.toLocaleString()} | ${finalCmds} cmds | ${formatUptime()}` +
|
|
3102
3132
|
(totalRecoveries > 0 ? ` | ${totalRecoveries} auto-recoveries` : '') +
|
|
3103
3133
|
(CLUSTER_ENABLED ? ` | node: ${NODE_ID.substring(0, 12)}` : '');
|
|
3104
3134
|
sendWebhook('Session Ended', webhookMsg, 0x8b5cf6);
|
|
3105
3135
|
|
|
3106
|
-
if (totalRecoveries > 0 || totalDisconnects > 0) {
|
|
3107
|
-
console.log(` ${c.dim}Recovery stats: ${totalRecoveries} auto-recoveries, ${totalDisconnects} disconnects, ${totalRateLimits} rate limits${c.reset}`);
|
|
3108
|
-
}
|
|
3109
|
-
if (CLUSTER_ENABLED) {
|
|
3110
|
-
console.log(` ${c.dim}Cluster node: ${NODE_ID} — claims released${c.reset}`);
|
|
3111
|
-
}
|
|
3112
|
-
|
|
3113
3136
|
if (redis) { try { redis.disconnect(); } catch {} }
|
|
3114
|
-
setTimeout(() => process.exit(0),
|
|
3137
|
+
setTimeout(() => process.exit(0), 500);
|
|
3115
3138
|
});
|
|
3116
3139
|
}
|
|
3117
3140
|
|
package/lib/terminal.js
ADDED
|
@@ -0,0 +1,739 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* terminal.js — Modern animated terminal renderer for DankGrinder
|
|
3
|
+
*
|
|
4
|
+
* Key design:
|
|
5
|
+
* - stdout capture during startup → buffer prevents bleed-through
|
|
6
|
+
* - When setActive() called: clear screen + replay buffer, then normal
|
|
7
|
+
* - After activation: all w.log() routed through flashEvent()
|
|
8
|
+
* - Virtual window: single-line per account (scales to 10k+)
|
|
9
|
+
* - 4 FPS render loop with dirty-row tracking (no flicker)
|
|
10
|
+
* - Graceful degradation: falls back to plain console.log if not TTY
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const READY = (() => {
|
|
14
|
+
try {
|
|
15
|
+
return process.stdout.isTTY && !process.env.NO_TERM;
|
|
16
|
+
} catch (_) { return false; }
|
|
17
|
+
})();
|
|
18
|
+
|
|
19
|
+
// ── ANSI Helpers ────────────────────────────────────────────────────────────
|
|
20
|
+
|
|
21
|
+
const A = {
|
|
22
|
+
reset: '\x1b[0m',
|
|
23
|
+
bold: '\x1b[1m',
|
|
24
|
+
dim: '\x1b[2m',
|
|
25
|
+
italic: '\x1b[3m',
|
|
26
|
+
|
|
27
|
+
black: '\x1b[30m', red: '\x1b[31m', green: '\x1b[32m',
|
|
28
|
+
yellow: '\x1b[33m', blue: '\x1b[34m', magenta: '\x1b[35m',
|
|
29
|
+
cyan: '\x1b[36m', white: '\x1b[37m',
|
|
30
|
+
|
|
31
|
+
bgBlack: '\x1b[40m', bgRed: '\x1b[41m', bgGreen: '\x1b[42m',
|
|
32
|
+
bgYellow: '\x1b[43m', bgBlue: '\x1b[44m', bgMagenta: '\x1b[45m',
|
|
33
|
+
bgCyan: '\x1b[46m', bgWhite: '\x1b[47m',
|
|
34
|
+
|
|
35
|
+
// True-color RGB
|
|
36
|
+
rgb: (r, g, b) => `\x1b[38;2;${r};${g};${b}m`,
|
|
37
|
+
|
|
38
|
+
// Cursor
|
|
39
|
+
save: '\x1b7', restore: '\x1b8',
|
|
40
|
+
hide: '\x1b[?25l', show: '\x1b[?25h',
|
|
41
|
+
up: (n = 1) => `\x1b[${n}A`,
|
|
42
|
+
down: (n = 1) => `\x1b[${n}B`,
|
|
43
|
+
clear: '\x1b[2J',
|
|
44
|
+
clearLine: '\x1b[2K',
|
|
45
|
+
home: '\x1b[H',
|
|
46
|
+
|
|
47
|
+
// Erase in display (clears scrollback too)
|
|
48
|
+
eraseAll: '\x1b[3J\x1b[2J\x1b[H',
|
|
49
|
+
|
|
50
|
+
// 256-color shortcuts
|
|
51
|
+
purple: '\x1b[38;5;141m',
|
|
52
|
+
pink: '\x1b[38;5;205m',
|
|
53
|
+
orange: '\x1b[38;5;214m',
|
|
54
|
+
teal: '\x1b[38;5;44m',
|
|
55
|
+
lime: '\x1b[38;5;82m',
|
|
56
|
+
crimson: '\x1b[38;5;196m',
|
|
57
|
+
slate: '\x1b[38;5;245m',
|
|
58
|
+
gold: '\x1b[38;5;220m',
|
|
59
|
+
emerald: '\x1b[38;5;48m',
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
// ── Color scheme ────────────────────────────────────────────────────────────
|
|
63
|
+
|
|
64
|
+
const C = {
|
|
65
|
+
header: A.purple,
|
|
66
|
+
headerDim: A.rgb(100, 70, 180),
|
|
67
|
+
border: A.rgb(55, 45, 85),
|
|
68
|
+
borderDim: A.rgb(35, 30, 55),
|
|
69
|
+
|
|
70
|
+
name: A.rgb(220, 215, 255),
|
|
71
|
+
nameDim: A.slate,
|
|
72
|
+
coins: A.gold,
|
|
73
|
+
level: A.cyan,
|
|
74
|
+
lifesavers: A.pink,
|
|
75
|
+
lifesaversLow: A.crimson,
|
|
76
|
+
lifesaversMid: A.orange,
|
|
77
|
+
|
|
78
|
+
statusActive: A.emerald,
|
|
79
|
+
statusPaused: A.crimson,
|
|
80
|
+
statusWarning: A.orange,
|
|
81
|
+
statusOffline: A.slate,
|
|
82
|
+
statusConnecting: A.yellow,
|
|
83
|
+
|
|
84
|
+
cmdSuccess: A.emerald,
|
|
85
|
+
cmdError: A.crimson,
|
|
86
|
+
cmdEvent: A.purple,
|
|
87
|
+
cmdWarn: A.orange,
|
|
88
|
+
cmdInfo: A.slate,
|
|
89
|
+
|
|
90
|
+
statLabel: A.slate,
|
|
91
|
+
statValue: A.white,
|
|
92
|
+
|
|
93
|
+
// Box styles
|
|
94
|
+
topLeft: '╭', topRight: '╮',
|
|
95
|
+
botLeft: '╰', botRight: '╯',
|
|
96
|
+
h: '─', v: '│',
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
// ── Spinner frames ──────────────────────────────────────────────────────────
|
|
100
|
+
|
|
101
|
+
const SPIN = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠿'];
|
|
102
|
+
|
|
103
|
+
// ── stdout capture during startup ─────────────────────────────────────────
|
|
104
|
+
|
|
105
|
+
let _origWrite = null;
|
|
106
|
+
let _captureActive = false;
|
|
107
|
+
let _captureBuf = [];
|
|
108
|
+
|
|
109
|
+
function _captureWrite(chunk) {
|
|
110
|
+
if (_captureActive) {
|
|
111
|
+
_captureBuf.push(String(chunk));
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
return _origWrite.call(process.stdout, chunk);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// ── Terminal Renderer ────────────────────────────────────────────────────────
|
|
118
|
+
|
|
119
|
+
class Terminal {
|
|
120
|
+
constructor() {
|
|
121
|
+
this.workers = [];
|
|
122
|
+
this.events = [];
|
|
123
|
+
this.MAX_EVENTS = 4;
|
|
124
|
+
|
|
125
|
+
this.phaseName = '';
|
|
126
|
+
this.phaseFrame = 0;
|
|
127
|
+
this.phaseTimer = null;
|
|
128
|
+
|
|
129
|
+
this.dirtyWorkers = new Set();
|
|
130
|
+
this.dirtyEvents = false;
|
|
131
|
+
this.dirtyStats = true;
|
|
132
|
+
this._renderTimer = null;
|
|
133
|
+
this._startTime = 0;
|
|
134
|
+
|
|
135
|
+
this.windowStart = 0;
|
|
136
|
+
this.windowSize = 8;
|
|
137
|
+
this._followIdx = -1;
|
|
138
|
+
|
|
139
|
+
this._lineCount = 0;
|
|
140
|
+
this._active = false;
|
|
141
|
+
this._shutdown = false;
|
|
142
|
+
|
|
143
|
+
this._w = 80;
|
|
144
|
+
this._h = 24;
|
|
145
|
+
this._resizeTimer = null;
|
|
146
|
+
|
|
147
|
+
// ── Startup capture ──
|
|
148
|
+
this._capturing = false;
|
|
149
|
+
this._origLog = null;
|
|
150
|
+
this._phaseProgressDone = 0;
|
|
151
|
+
this._phaseProgressTotal = 0;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// ── Public API ──────────────────────────────────────────────────────────
|
|
155
|
+
|
|
156
|
+
init(opts = {}) {
|
|
157
|
+
this._startTime = opts.startTime || Date.now();
|
|
158
|
+
this.workers = opts.workers || [];
|
|
159
|
+
this._updateSize();
|
|
160
|
+
|
|
161
|
+
if (READY) {
|
|
162
|
+
// Override stdout.write to capture everything during startup
|
|
163
|
+
_origWrite = process.stdout.write.bind(process.stdout);
|
|
164
|
+
process.stdout.write = _captureWrite;
|
|
165
|
+
this._capturing = true;
|
|
166
|
+
_captureActive = true;
|
|
167
|
+
_captureBuf = [];
|
|
168
|
+
|
|
169
|
+
// Also override console.log temporarily
|
|
170
|
+
this._origLog = console.log;
|
|
171
|
+
console.log = (...args) => {
|
|
172
|
+
if (this._active) {
|
|
173
|
+
// After activation, route to flashEvent
|
|
174
|
+
const msg = args.join(' ').replace(/\x1b\[[0-9;]*m/g, '').substring(0, 120);
|
|
175
|
+
if (msg.trim()) this.flashEvent('info', msg);
|
|
176
|
+
} else {
|
|
177
|
+
_captureWrite(args.join(' ') + '\n');
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
process.stdout.on('resize', this._onResize.bind(this));
|
|
182
|
+
this._drawStartupScreen();
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
setVersion(v) { this._version = v; }
|
|
187
|
+
|
|
188
|
+
startPhase(name) {
|
|
189
|
+
this.phaseName = name;
|
|
190
|
+
this.phaseFrame = 0;
|
|
191
|
+
this._phaseProgressDone = 0;
|
|
192
|
+
this._phaseProgressTotal = 0;
|
|
193
|
+
|
|
194
|
+
if (!READY) {
|
|
195
|
+
console.log(` ⟳ ${name}...`);
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
if (this.phaseTimer) clearInterval(this.phaseTimer);
|
|
199
|
+
this.phaseTimer = setInterval(() => {
|
|
200
|
+
this.phaseFrame = (this.phaseFrame + 1) % SPIN.length;
|
|
201
|
+
this._redrawPhaseSpinner();
|
|
202
|
+
}, 120);
|
|
203
|
+
this._redrawPhaseSpinner();
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
updateProgress(done, total) {
|
|
207
|
+
this._phaseProgressDone = done;
|
|
208
|
+
this._phaseProgressTotal = total;
|
|
209
|
+
if (!READY) return;
|
|
210
|
+
this._redrawProgressBar();
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
endPhase(name, ok = true) {
|
|
214
|
+
if (this.phaseTimer) { clearInterval(this.phaseTimer); this.phaseTimer = null; }
|
|
215
|
+
this.phaseName = '';
|
|
216
|
+
this._phaseProgressDone = 0;
|
|
217
|
+
this._phaseProgressTotal = 0;
|
|
218
|
+
|
|
219
|
+
if (!READY) {
|
|
220
|
+
const icon = ok ? `✓ ${name}` : `✗ ${name}`;
|
|
221
|
+
console.log(` ${icon}`);
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
const icon = ok ? `${C.header}✓${A.reset}` : `${A.crimson}✗${A.reset}`;
|
|
225
|
+
const line = ` ${icon} ${name}`;
|
|
226
|
+
const w = this._w;
|
|
227
|
+
|
|
228
|
+
// Clear spinner + progress rows, show result
|
|
229
|
+
this._write(
|
|
230
|
+
A.save +
|
|
231
|
+
this._cursor(6, 1) + A.clearLine +
|
|
232
|
+
this._cursor(7, 1) + A.clearLine +
|
|
233
|
+
this._cursor(8, 1) + A.clearLine +
|
|
234
|
+
this._cursor(9, 1) + A.clearLine +
|
|
235
|
+
this._cursor(10, 1) + A.clearLine +
|
|
236
|
+
this._cursor(11, 1) + A.clearLine +
|
|
237
|
+
this._cursor(12, 1) + A.clearLine +
|
|
238
|
+
`${this._rpad(line, w)}` +
|
|
239
|
+
A.restore
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
flashEvent(type, msg) {
|
|
244
|
+
const now = new Date();
|
|
245
|
+
const ts = `${A.dim}${String(now.getMinutes()).padStart(2, '0')}:${String(now.getSeconds()).padStart(2, '0')}${A.reset}`;
|
|
246
|
+
this.events.unshift({ ts, type, msg, id: Date.now() });
|
|
247
|
+
if (this.events.length > this.MAX_EVENTS) this.events.pop();
|
|
248
|
+
this.dirtyEvents = true;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
setWorkers(workers) {
|
|
252
|
+
this.workers = workers;
|
|
253
|
+
this.dirtyWorkers = new Set(workers.map((_, i) => i));
|
|
254
|
+
this.dirtyStats = true;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
markWorkerDirty(idx) {
|
|
258
|
+
this.dirtyWorkers.add(idx);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
setActive() {
|
|
262
|
+
if (this._active) return;
|
|
263
|
+
this._active = true;
|
|
264
|
+
|
|
265
|
+
if (READY) {
|
|
266
|
+
// ── Stop capturing, fully clear screen, draw live view ──
|
|
267
|
+
_captureActive = false;
|
|
268
|
+
this._capturing = false;
|
|
269
|
+
|
|
270
|
+
// Restore stdout.write first
|
|
271
|
+
if (_origWrite) {
|
|
272
|
+
process.stdout.write = _origWrite;
|
|
273
|
+
_origWrite = null;
|
|
274
|
+
}
|
|
275
|
+
if (this._origLog) {
|
|
276
|
+
console.log = this._origLog;
|
|
277
|
+
this._origLog = null;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Discard all buffered startup output — we only want the live view
|
|
281
|
+
_captureBuf = [];
|
|
282
|
+
|
|
283
|
+
// Full screen clear + scrollback clear
|
|
284
|
+
this._write(A.eraseAll);
|
|
285
|
+
|
|
286
|
+
// Draw live view
|
|
287
|
+
this._drawLiveView();
|
|
288
|
+
|
|
289
|
+
// Reset dirty flags so we don't redraw header every frame
|
|
290
|
+
this.dirtyWorkers.clear();
|
|
291
|
+
this.dirtyEvents = false;
|
|
292
|
+
this.dirtyStats = false;
|
|
293
|
+
|
|
294
|
+
this._startRenderLoop();
|
|
295
|
+
} else {
|
|
296
|
+
// Non-TTY: restore console.log
|
|
297
|
+
if (this._origLog) {
|
|
298
|
+
console.log = this._origLog;
|
|
299
|
+
this._origLog = null;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
scrollBy(delta) {
|
|
305
|
+
if (!READY || this._shutdown) return;
|
|
306
|
+
const max = Math.max(0, this.workers.length - this.windowSize);
|
|
307
|
+
this.windowStart = Math.max(0, Math.min(max, this.windowStart + delta));
|
|
308
|
+
this._followIdx = -1;
|
|
309
|
+
this.dirtyWorkers = new Set();
|
|
310
|
+
for (let i = this.windowStart; i < this.windowStart + this.windowSize; i++) {
|
|
311
|
+
if (i < this.workers.length) this.dirtyWorkers.add(i);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
shutdown(summary = {}) {
|
|
316
|
+
this._shutdown = true;
|
|
317
|
+
if (this._renderTimer) { clearInterval(this._renderTimer); this._renderTimer = null; }
|
|
318
|
+
if (this._phaseTimer) { clearInterval(this._phaseTimer); this._phaseTimer = null; }
|
|
319
|
+
|
|
320
|
+
// Restore stdout state
|
|
321
|
+
if (READY && _origWrite) {
|
|
322
|
+
process.stdout.write = _origWrite;
|
|
323
|
+
_origWrite = null;
|
|
324
|
+
}
|
|
325
|
+
if (this._origLog) {
|
|
326
|
+
console.log = this._origLog;
|
|
327
|
+
this._origLog = null;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
this._write(A.show);
|
|
331
|
+
|
|
332
|
+
const w = this._w;
|
|
333
|
+
const { totalCoins = 0, totalCmds = 0, totalSuccess = 0,
|
|
334
|
+
workers = [], uptime = 0, memMB = 0 } = summary;
|
|
335
|
+
|
|
336
|
+
const b = C.border;
|
|
337
|
+
const h = C.header;
|
|
338
|
+
const g = C.statValue;
|
|
339
|
+
const dim = A.dim;
|
|
340
|
+
const r = A.reset;
|
|
341
|
+
|
|
342
|
+
let out = '';
|
|
343
|
+
out += A.eraseAll + A.home + A.save;
|
|
344
|
+
|
|
345
|
+
// Box top
|
|
346
|
+
out += `${this._at(1, 1)}${b}${C.topLeft}${'─'.repeat(w - 2)}${C.topRight}${r}`;
|
|
347
|
+
out += `${this._at(2, 1)}${b}${C.v} ${h}${A.bold} ⬡ DANKGRINDER — Session Summary ${r}${' '.repeat(Math.max(0, w - 40))}${C.v}${r}`;
|
|
348
|
+
out += `${this._at(3, 1)}${b}${C.h}${'─'.repeat(w - 2)}${C.h}${r}`;
|
|
349
|
+
|
|
350
|
+
// Column headers
|
|
351
|
+
const hdr = [
|
|
352
|
+
`${h}#${r}`, `${h}ACCOUNT${r}`, `${h}COINS${r}`,
|
|
353
|
+
`${h}LV${r}`, `${h}♥${r}`, `${h}CMDS${r}`,
|
|
354
|
+
`${h}OK%${r}`, `${h}STATUS${r}`,
|
|
355
|
+
].join(` `);
|
|
356
|
+
out += `${this._at(4, 1)}${b} ${this._rpad(hdr, w - 4)} ${C.v}${r}`;
|
|
357
|
+
out += `${this._at(5, 1)}${b}${C.h}${'─'.repeat(w - 2)}${C.h}${r}`;
|
|
358
|
+
|
|
359
|
+
// Per-account rows
|
|
360
|
+
let row = 6;
|
|
361
|
+
for (let i = 0; i < workers.length && row < this._h - 3; i++) {
|
|
362
|
+
const wk = workers[i];
|
|
363
|
+
const rate = wk.stats?.commands > 0
|
|
364
|
+
? `${((wk.stats.successes / wk.stats.commands) * 100).toFixed(0)}%`
|
|
365
|
+
: '0%';
|
|
366
|
+
const ls = wk._lifesavers ?? '?';
|
|
367
|
+
const lsColor = ls === 0 ? C.lifesaversLow : ls <= 2 ? C.lifesaversMid : C.lifesavers;
|
|
368
|
+
const statusIcon = (!wk.running || wk._tokenInvalid) ? '⚫ offline'
|
|
369
|
+
: wk.paused || wk.dashboardPaused ? '🔴 paused'
|
|
370
|
+
: '🟢 active';
|
|
371
|
+
|
|
372
|
+
const line = [
|
|
373
|
+
`${g}${String(i + 1).padEnd(2)}${r}`,
|
|
374
|
+
`${C.name}${A.bold}${(wk.username || '?').substring(0, 18).padEnd(19)}${r}`,
|
|
375
|
+
`${C.coins}⏣${(wk.stats?.coins || 0).toLocaleString().padStart(8)}${r}`,
|
|
376
|
+
`${C.level}Lv.${wk._level ?? '?'}${r}`,
|
|
377
|
+
`${lsColor}♥${String(ls).padStart(2)}${r}`,
|
|
378
|
+
`${g}${String(wk.stats?.commands || 0).padStart(4)}cmds${r}`,
|
|
379
|
+
`${g}${rate.padStart(4)}${r}`,
|
|
380
|
+
`${g}${statusIcon}${r}`,
|
|
381
|
+
].join(' ');
|
|
382
|
+
out += `${this._at(row++, 1)}${b} ${this._rpad(line, w - 4)} ${C.v}${r}`;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
row++; // blank
|
|
386
|
+
out += `${this._at(row++, 1)}${b}${C.h}${'─'.repeat(w - 2)}${C.h}${r}`;
|
|
387
|
+
|
|
388
|
+
// Totals row
|
|
389
|
+
const totalRate = totalCmds > 0 ? `${((totalSuccess / totalCmds) * 100).toFixed(0)}%` : '0%';
|
|
390
|
+
const uptimeStr = this._fmtUptime(uptime);
|
|
391
|
+
const totalLine = [
|
|
392
|
+
`${h}💰 TOTAL:${r}`,
|
|
393
|
+
`${C.coins}${A.bold}⏣ ${totalCoins.toLocaleString()}${r}`,
|
|
394
|
+
`${dim}${totalCmds} cmds${r}`,
|
|
395
|
+
`${dim}${totalRate} OK${r}`,
|
|
396
|
+
`${dim}${uptimeStr}${r}`,
|
|
397
|
+
`${dim}${memMB}MB RAM${r}`,
|
|
398
|
+
].join(' ');
|
|
399
|
+
out += `${this._at(row++, 1)}${b} ${this._rpad(totalLine, w - 4)} ${C.v}${r}`;
|
|
400
|
+
out += `${this._at(row, 1)}${b}${C.botLeft}${'─'.repeat(w - 2)}${C.botRight}${r}`;
|
|
401
|
+
out += A.restore + A.show;
|
|
402
|
+
|
|
403
|
+
this._write(out);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// ── Internal ────────────────────────────────────────────────────────────
|
|
407
|
+
|
|
408
|
+
_updateSize() {
|
|
409
|
+
try {
|
|
410
|
+
this._w = process.stdout.columns || 80;
|
|
411
|
+
this._h = process.stdout.rows || 24;
|
|
412
|
+
this.windowSize = Math.max(3, this._h - 11);
|
|
413
|
+
} catch (_) {
|
|
414
|
+
this._w = 80; this._h = 24; this.windowSize = 8;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
_onResize() {
|
|
419
|
+
if (this._resizeTimer) clearTimeout(this._resizeTimer);
|
|
420
|
+
this._resizeTimer = setTimeout(() => {
|
|
421
|
+
this._updateSize();
|
|
422
|
+
if (this._active) {
|
|
423
|
+
this._write(A.eraseAll);
|
|
424
|
+
this._drawLiveView();
|
|
425
|
+
}
|
|
426
|
+
}, 100);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
_ansiLen(s) {
|
|
430
|
+
let len = 0, i = 0;
|
|
431
|
+
const str = String(s);
|
|
432
|
+
while (i < str.length) {
|
|
433
|
+
if (str.charCodeAt(i) === 0x1b && str[i + 1] === '[') {
|
|
434
|
+
let j = i + 2;
|
|
435
|
+
while (j < str.length && str[j] !== 'm') j++;
|
|
436
|
+
i = j + 1;
|
|
437
|
+
} else { len++; i++; }
|
|
438
|
+
}
|
|
439
|
+
return len;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
_rpad(s, width) {
|
|
443
|
+
return s + ' '.repeat(Math.max(0, width - this._ansiLen(s)));
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
_cursor(row) { return `\x1b[${row};1H`; }
|
|
447
|
+
_at(row, col) { return `\x1b[${row};${col}H`; }
|
|
448
|
+
_write(str) { if (str) process.stdout.write(str); }
|
|
449
|
+
|
|
450
|
+
_fmtUptime(ms) {
|
|
451
|
+
if (!ms) return '0s';
|
|
452
|
+
const s = Math.floor(ms / 1000);
|
|
453
|
+
if (s < 60) return `${s}s`;
|
|
454
|
+
const m = Math.floor(s / 60);
|
|
455
|
+
if (m < 60) return `${m}m ${s % 60}s`;
|
|
456
|
+
const h = Math.floor(m / 60);
|
|
457
|
+
if (h < 24) return `${h}h ${m % 60}m`;
|
|
458
|
+
return `${Math.floor(h / 24)}d ${h % 24}h`;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
_fmtCoins(n) {
|
|
462
|
+
if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`;
|
|
463
|
+
if (n >= 1_000) return `${(n / 1_000).toFixed(1)}K`;
|
|
464
|
+
return String(n);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
_buildAccountRow(wk, idx) {
|
|
468
|
+
const w = this._w;
|
|
469
|
+
const ls = wk._lifesavers ?? '?';
|
|
470
|
+
const lsColor = ls === 0 ? C.lifesaversLow : ls <= 2 ? C.lifesaversMid : C.lifesavers;
|
|
471
|
+
const rate = wk.stats?.commands > 0
|
|
472
|
+
? `${((wk.stats.successes / wk.stats.commands) * 100).toFixed(0)}%`
|
|
473
|
+
: '0%';
|
|
474
|
+
|
|
475
|
+
let statusDot, statusText, rowBg;
|
|
476
|
+
if (!wk.running || wk._tokenInvalid) {
|
|
477
|
+
statusDot = '⚫'; statusText = 'offline';
|
|
478
|
+
} else if (wk.paused || wk.dashboardPaused) {
|
|
479
|
+
statusDot = '🔴'; statusText = 'paused';
|
|
480
|
+
} else if (wk.lastStatus?.includes('claim') || wk.lastStatus?.includes('daily')) {
|
|
481
|
+
statusDot = '🟡'; statusText = 'claiming';
|
|
482
|
+
} else {
|
|
483
|
+
statusDot = '🟢'; statusText = 'grinding';
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
const line = [
|
|
487
|
+
`${C.header}${idx + 1}.${A.reset}`,
|
|
488
|
+
`${C.name}${A.bold}${(wk.username || '?').substring(0, 18).padEnd(19)}${A.reset}`,
|
|
489
|
+
`${C.coins}⏣${this._fmtCoins(wk.stats?.coins || 0).padStart(7)}${A.reset}`,
|
|
490
|
+
`${C.level}Lv.${String(wk._level ?? '?').padStart(3)}${A.reset}`,
|
|
491
|
+
`${lsColor}♥${String(ls).padStart(2)}${A.reset}`,
|
|
492
|
+
`${C.statValue}${String(wk.stats?.commands || 0).padStart(4)}cmds${A.reset}`,
|
|
493
|
+
`${C.statValue}${rate.padStart(5)}${A.reset}`,
|
|
494
|
+
`${statusDot} ${statusText}`.padEnd(14),
|
|
495
|
+
`${A.dim}${(wk.lastStatus || '').substring(0, 22).padEnd(22)}${A.reset}`,
|
|
496
|
+
].join(' ');
|
|
497
|
+
|
|
498
|
+
return this._rpad(line, w);
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
_drawStartupScreen() {
|
|
502
|
+
const w = this._w;
|
|
503
|
+
const b = C.border;
|
|
504
|
+
const h = C.header;
|
|
505
|
+
const dim = A.dim;
|
|
506
|
+
const r = A.reset;
|
|
507
|
+
|
|
508
|
+
let out = '';
|
|
509
|
+
out += A.eraseAll + A.home;
|
|
510
|
+
out += `${this._at(1, 1)}${b}${C.topLeft}${'─'.repeat(w - 2)}${C.topRight}${r}`;
|
|
511
|
+
out += `${this._at(2, 1)}${b} ${h}${A.bold} ⬡ DANKGRINDER v${this._version || '?'} ${r}${'─'.repeat(Math.max(0, w - 28 - (this._version || '').length))}${r}`;
|
|
512
|
+
out += `${this._at(3, 1)}${b}${C.h}${'─'.repeat(w - 2)}${C.h}${r}`;
|
|
513
|
+
// Status bar placeholder
|
|
514
|
+
out += `${this._at(4, 1)}${b}${' '.repeat(w - 2)}${r}`;
|
|
515
|
+
out += `${this._at(5, 1)}${b}${C.h}${'─'.repeat(w - 2)}${C.h}${r}`;
|
|
516
|
+
// Spinner area
|
|
517
|
+
out += `${this._at(7, 1)}${b}${' '.repeat(w - 2)}${r}`;
|
|
518
|
+
out += `${this._at(8, 1)}${b}${' '.repeat(w - 2)}${r}`;
|
|
519
|
+
out += `${this._at(9, 1)}${b}${' '.repeat(w - 2)}${r}`;
|
|
520
|
+
out += `${this._at(10, 1)}${b}${' '.repeat(w - 2)}${r}`;
|
|
521
|
+
out += `${this._at(11, 1)}${b}${' '.repeat(w - 2)}${r}`;
|
|
522
|
+
out += `${this._at(12, 1)}${b}${' '.repeat(w - 2)}${r}`;
|
|
523
|
+
// Footer
|
|
524
|
+
out += `${this._at(14, 1)}${b}${C.botLeft}${'─'.repeat(w - 2)}${C.botRight}${r}`;
|
|
525
|
+
out += `${this._at(15, 1)}${b} ${dim}Starting up...${r}${' '.repeat(Math.max(0, w - 18))}${r}`;
|
|
526
|
+
|
|
527
|
+
this._write(out);
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
_redrawPhaseSpinner() {
|
|
531
|
+
if (!READY || !this._phaseName) return;
|
|
532
|
+
const frame = SPIN[this.phaseFrame];
|
|
533
|
+
const line = ` ${frame} ${this.phaseName}...`;
|
|
534
|
+
const w = this._w;
|
|
535
|
+
const b = C.border;
|
|
536
|
+
const r = A.reset;
|
|
537
|
+
this._write(
|
|
538
|
+
A.save +
|
|
539
|
+
this._cursor(8, 1) + A.clearLine +
|
|
540
|
+
`${b} ${line}${' '.repeat(Math.max(0, w - this._ansiLen(line) - 3))}${r}` +
|
|
541
|
+
A.restore
|
|
542
|
+
);
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
_redrawProgressBar() {
|
|
546
|
+
if (!READY || !this._phaseName) return;
|
|
547
|
+
const { done, total } = { done: this._phaseProgressDone, total: this._phaseProgressTotal };
|
|
548
|
+
const w = this._w;
|
|
549
|
+
const h = C.header;
|
|
550
|
+
const dim = A.dim;
|
|
551
|
+
const r = A.reset;
|
|
552
|
+
|
|
553
|
+
const barW = Math.max(10, w - 35);
|
|
554
|
+
const filled = total > 0 ? Math.round((done / total) * barW) : 0;
|
|
555
|
+
const bar = `${h}${'█'.repeat(filled)}${dim}${'░'.repeat(barW - filled)}${r}`;
|
|
556
|
+
const label = ` ${done}/${total} `;
|
|
557
|
+
const line = bar + label;
|
|
558
|
+
this._write(
|
|
559
|
+
A.save +
|
|
560
|
+
this._cursor(9, 1) + A.clearLine +
|
|
561
|
+
`${C.border} ${line}${' '.repeat(Math.max(0, w - this._ansiLen(line) - 3))}${r}` +
|
|
562
|
+
A.restore
|
|
563
|
+
);
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
_drawLiveView() {
|
|
567
|
+
this._drawHeader();
|
|
568
|
+
this._drawAccounts();
|
|
569
|
+
this._drawEvents();
|
|
570
|
+
this._drawFooter();
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
_drawHeader() {
|
|
574
|
+
const w = this._w;
|
|
575
|
+
const b = C.border;
|
|
576
|
+
const h = C.header;
|
|
577
|
+
const g = C.statValue;
|
|
578
|
+
const dim = A.dim;
|
|
579
|
+
const r = A.reset;
|
|
580
|
+
|
|
581
|
+
// Title bar
|
|
582
|
+
const titleText = ` ⬡ DANKGRINDER v${this._version || '?'} `;
|
|
583
|
+
const titlePad = Math.max(0, w - 2 - this._ansiLen(titleText));
|
|
584
|
+
this._write(`${this._at(1, 1)}${b}${C.topLeft}${'─'.repeat(w - 2)}${C.topRight}${r}`);
|
|
585
|
+
this._write(`${this._at(2, 1)}${b} ${h}${A.bold}${titleText}${r}${' '.repeat(titlePad)} ${C.v}${r}`);
|
|
586
|
+
this._write(`${this._at(3, 1)}${b}${C.h}${'─'.repeat(w - 2)}${C.h}${r}`);
|
|
587
|
+
|
|
588
|
+
// Stats bar
|
|
589
|
+
const stats = this._buildStatsLine();
|
|
590
|
+
const statsPad = Math.max(0, w - 2 - this._ansiLen(stats) - 2);
|
|
591
|
+
this._write(`${this._at(4, 1)}${b} ${stats}${' '.repeat(statsPad)} ${C.v}${r}`);
|
|
592
|
+
this._write(`${this._at(5, 1)}${b}${C.h}${'─'.repeat(w - 2)}${C.h}${r}`);
|
|
593
|
+
|
|
594
|
+
this._accountsRow = 6;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
_buildStatsLine() {
|
|
598
|
+
let totalCoins = 0, totalCmds = 0, totalSuccess = 0, totalLs = 0;
|
|
599
|
+
let paused = 0, active = 0;
|
|
600
|
+
|
|
601
|
+
for (const wk of this.workers) {
|
|
602
|
+
totalCoins += wk.stats?.coins || 0;
|
|
603
|
+
totalCmds += wk.stats?.commands || 0;
|
|
604
|
+
totalSuccess += wk.stats?.successes || 0;
|
|
605
|
+
if (wk._lifesavers != null) totalLs += wk._lifesavers;
|
|
606
|
+
if (wk.running && !wk._tokenInvalid) {
|
|
607
|
+
if (wk.paused || wk.dashboardPaused) paused++;
|
|
608
|
+
else active++;
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
const uptime = this._fmtUptime(Date.now() - this._startTime);
|
|
613
|
+
const rate = totalCmds > 0 ? ((totalSuccess / totalCmds) * 100).toFixed(0) : '0';
|
|
614
|
+
|
|
615
|
+
return [
|
|
616
|
+
`${A.dim}⏱${A.reset} ${C.statValue}${uptime}${A.reset}`,
|
|
617
|
+
`${A.dim}⬡${A.reset} ${C.statValue}${this.workers.length}${A.reset} ${A.dim}accounts${A.reset}`,
|
|
618
|
+
`${C.coins}⏣${A.reset} ${C.statValue}${totalCoins.toLocaleString()}${A.reset}`,
|
|
619
|
+
`${A.dim}⚡${A.reset} ${C.statValue}${totalCmds}${A.reset} ${A.dim}cmds${A.reset}`,
|
|
620
|
+
`${A.dim}📊${A.reset} ${C.statValue}${rate}%${A.reset}`,
|
|
621
|
+
`${C.lifesavers}♥${A.reset} ${C.statValue}${totalLs}${A.reset}`,
|
|
622
|
+
`${C.statusActive}🟢${A.reset} ${C.statValue}${active}${A.reset}`,
|
|
623
|
+
`${C.statusPaused}🔴${A.reset} ${C.statValue}${paused}${A.reset}`,
|
|
624
|
+
].join(` │ `);
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
_drawAccounts() {
|
|
628
|
+
const w = this._w;
|
|
629
|
+
const b = C.border;
|
|
630
|
+
const h = C.header;
|
|
631
|
+
const r = A.reset;
|
|
632
|
+
|
|
633
|
+
// Column headers
|
|
634
|
+
const cols = [
|
|
635
|
+
`${h}#${r}`, `${h}ACCOUNT${r}`, `${h}COINS${r}`,
|
|
636
|
+
`${h}LV${r}`, `${h}♥${r}`, `${h}CMDS${r}`,
|
|
637
|
+
`${h}OK%${r}`, `${h}STATUS${r}`,
|
|
638
|
+
].join(` `);
|
|
639
|
+
this._write(`${this._at(this._accountsRow, 1)}${b} ${this._rpad(cols, w - 4)} ${C.v}${r}`);
|
|
640
|
+
this._write(`${this._at(this._accountsRow + 1, 1)}${b}${C.h}${'─'.repeat(w - 2)}${C.h}${r}`);
|
|
641
|
+
|
|
642
|
+
const visible = this.workers.slice(this.windowStart, this.windowStart + this.windowSize);
|
|
643
|
+
for (let i = 0; i < this.windowSize; i++) {
|
|
644
|
+
const row = this._accountsRow + 2 + i;
|
|
645
|
+
if (row > this._h - 4) break;
|
|
646
|
+
if (i < visible.length) {
|
|
647
|
+
const line = this._buildAccountRow(visible[i], this.windowStart + i);
|
|
648
|
+
this._write(`${this._at(row, 1)}${b} ${line} ${C.v}${r}`);
|
|
649
|
+
} else {
|
|
650
|
+
this._write(`${this._at(row, 1)}${b}${' '.repeat(w - 2)}${r}`);
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
this._eventsRow = this._accountsRow + 2 + Math.min(this.windowSize, this.workers.length);
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
_drawEvents() {
|
|
657
|
+
const w = this._w;
|
|
658
|
+
const b = C.border;
|
|
659
|
+
const dim = A.dim;
|
|
660
|
+
const r = A.reset;
|
|
661
|
+
|
|
662
|
+
if (this._eventsRow > this._h - 4) return;
|
|
663
|
+
this._write(`${this._at(this._eventsRow, 1)}${b}${C.h}${'─'.repeat(w - 2)}${C.h}${r}`);
|
|
664
|
+
|
|
665
|
+
const visible = this.events.slice(0, Math.min(this.MAX_EVENTS, this._h - this._eventsRow - 3));
|
|
666
|
+
for (let i = 0; i < visible.length; i++) {
|
|
667
|
+
const row = this._eventsRow + 1 + i;
|
|
668
|
+
if (row > this._h - 2) break;
|
|
669
|
+
const e = visible[i];
|
|
670
|
+
const typeColor = e.type === 'death' ? C.cmdError
|
|
671
|
+
: e.type === 'lowls' ? C.cmdWarn
|
|
672
|
+
: e.type === 'levelup' ? C.cmdSuccess
|
|
673
|
+
: e.type === 'success' ? C.cmdSuccess
|
|
674
|
+
: C.cmdInfo;
|
|
675
|
+
const line = ` ${dim}${e.ts}${A.reset} ${typeColor}${e.msg}${A.reset}`;
|
|
676
|
+
this._write(`${this._at(row, 1)}${b} ${this._rpad(line, w - 4)} ${C.v}${r}`);
|
|
677
|
+
}
|
|
678
|
+
this._footerRow = this._eventsRow + 1 + visible.length;
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
_drawFooter() {
|
|
682
|
+
const w = this._w;
|
|
683
|
+
const b = C.border;
|
|
684
|
+
const dim = A.dim;
|
|
685
|
+
const r = A.reset;
|
|
686
|
+
if (this._footerRow > this._h - 1) this._footerRow = this._h - 2;
|
|
687
|
+
this._write(`${this._at(this._footerRow, 1)}${b}${C.botLeft}${'─'.repeat(w - 2)}${C.botRight}${r}`);
|
|
688
|
+
const hint = `${dim}↑↓ scroll${r}`;
|
|
689
|
+
this._write(`${this._at(this._footerRow + 1, 1)}${b} ${hint}${' '.repeat(Math.max(0, w - 2 - this._ansiLen(hint)))}${r}`);
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
_render() {
|
|
693
|
+
if (!READY || this._shutdown || !this._active) return;
|
|
694
|
+
|
|
695
|
+
// Only update stats line (row 4) when dirty — don't redraw whole header
|
|
696
|
+
if (this.dirtyStats) {
|
|
697
|
+
const w = this._w;
|
|
698
|
+
const b = C.border;
|
|
699
|
+
const stats = this._buildStatsLine();
|
|
700
|
+
const statsPad = Math.max(0, w - 2 - this._ansiLen(stats) - 2);
|
|
701
|
+
this._write(`${this._at(4, 1)}${b} ${stats}${' '.repeat(statsPad)} ${C.v}${A.reset}`);
|
|
702
|
+
this.dirtyStats = false;
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
if (this.dirtyWorkers.size > 0) {
|
|
706
|
+
const w = this._w;
|
|
707
|
+
const b = C.border;
|
|
708
|
+
const r = A.reset;
|
|
709
|
+
for (const idx of this.dirtyWorkers) {
|
|
710
|
+
const localIdx = idx - this.windowStart;
|
|
711
|
+
const row = this._accountsRow + 2 + localIdx;
|
|
712
|
+
if (row < this._accountsRow + 2 || row > this._h - 4) continue;
|
|
713
|
+
if (idx < this.workers.length) {
|
|
714
|
+
const line = this._buildAccountRow(this.workers[idx], idx);
|
|
715
|
+
this._write(`${this._at(row, 1)}${b} ${line} ${C.v}${r}`);
|
|
716
|
+
} else {
|
|
717
|
+
this._write(`${this._at(row, 1)}${b}${' '.repeat(w - 2)}${r}`);
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
if (this.dirtyEvents) {
|
|
723
|
+
this._drawEvents();
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
this.dirtyWorkers.clear();
|
|
727
|
+
this.dirtyEvents = false;
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
_startRenderLoop() {
|
|
731
|
+
if (this._renderTimer) clearInterval(this._renderTimer);
|
|
732
|
+
this._renderTimer = setInterval(() => {
|
|
733
|
+
this.dirtyStats = true; // always refresh uptime/stats
|
|
734
|
+
this._render();
|
|
735
|
+
}, 250);
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
module.exports = new Terminal();
|