dankgrinder 7.77.0 → 7.78.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 +57 -45
- package/lib/terminal.js +820 -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,
|
|
@@ -1623,6 +1624,8 @@ class AccountWorker {
|
|
|
1623
1624
|
// ── Death / lifesaver detection in command responses ──
|
|
1624
1625
|
if (resultLower.includes('you died') || resultLower.includes('lifesaver protected')) {
|
|
1625
1626
|
this.log('error', `DEATH DETECTED during ${cmdName}!`);
|
|
1627
|
+
terminal.flashEvent('death', `💀 ${this.username} died — ${lsCount === 0 ? '0 lifesavers!' : (lsCount > 0 ? `${lsCount} lifesavers left` : 'check DM')}`);
|
|
1628
|
+
terminal.markWorkerDirty(this.idx);
|
|
1626
1629
|
// Check for lifesaver count in the response
|
|
1627
1630
|
const lsMatch = result.match(/(\d+)\s*life\s*saver/i);
|
|
1628
1631
|
const lsCount = lsMatch ? parseInt(lsMatch[1]) : -1;
|
|
@@ -1639,6 +1642,8 @@ class AccountWorker {
|
|
|
1639
1642
|
await redis.set(`dkg:lifesavers:${this.account.id}`, String(lsCount), 'EX', 86400);
|
|
1640
1643
|
this.log('warn', `Lifesaver used! ${lsCount} remaining.`);
|
|
1641
1644
|
if (lsCount <= 2) {
|
|
1645
|
+
terminal.flashEvent('lowls', `⚠ ${this.username} low lifesavers: ${lsCount} left`);
|
|
1646
|
+
terminal.markWorkerDirty(this.idx);
|
|
1642
1647
|
sendWebhook('LOW LIFESAVERS', `**${this.username}** has only **${lsCount}** lifesaver(s) left!`, 0xfbbf24);
|
|
1643
1648
|
}
|
|
1644
1649
|
}
|
|
@@ -1758,6 +1763,8 @@ class AccountWorker {
|
|
|
1758
1763
|
this.setStatus(formattedResult);
|
|
1759
1764
|
await sendLog(this.username, cmdName, result, 'success');
|
|
1760
1765
|
reportEarnings(this.account.id, this.username, earned, spent, cmdName);
|
|
1766
|
+
terminal.markWorkerDirty(this.idx);
|
|
1767
|
+
if (earned > 0) terminal.flashEvent('success', `⚔ ${this.username} ${cmdName}: +⏣ ${earned.toLocaleString()}`);
|
|
1761
1768
|
|
|
1762
1769
|
// Auto-sell fish every 5 fishing rounds
|
|
1763
1770
|
if (cmdName === 'fish') {
|
|
@@ -2844,6 +2851,8 @@ async function start(apiKey, apiUrl, opts = {}) {
|
|
|
2844
2851
|
w.log?.('error', `DEATH in DMs! 0 lifesavers — disabling crime/search`);
|
|
2845
2852
|
w.setCooldown?.('crime', 86400);
|
|
2846
2853
|
w.setCooldown?.('search', 86400);
|
|
2854
|
+
terminal.flashEvent('death', `💀 ${w.username} DEATH in DM — 0 lifesavers!`);
|
|
2855
|
+
terminal.markWorkerDirty(w.idx);
|
|
2847
2856
|
sendWebhook?.('DEATH ALERT (DM)', `**${w.username}** died in DMs! **0 lifesavers!**\nCrime/search auto-disabled.`, 0xef4444);
|
|
2848
2857
|
} else {
|
|
2849
2858
|
w.log?.('warn', `DEATH in DMs — ${event.lifesaversLeft} lifesavers remaining`);
|
|
@@ -2861,6 +2870,8 @@ async function start(apiKey, apiUrl, opts = {}) {
|
|
|
2861
2870
|
if (event.type === 'levelup') {
|
|
2862
2871
|
if (event.to > 0) {
|
|
2863
2872
|
w._level = event.to;
|
|
2873
|
+
terminal.flashEvent('levelup', `⬆️ ${w.username} leveled up to Lv.${event.to}`);
|
|
2874
|
+
terminal.markWorkerDirty(w.idx);
|
|
2864
2875
|
}
|
|
2865
2876
|
}
|
|
2866
2877
|
}
|
|
@@ -2876,8 +2887,12 @@ async function start(apiKey, apiUrl, opts = {}) {
|
|
|
2876
2887
|
console.log(` ${checks.join(' ')}`);
|
|
2877
2888
|
console.log('');
|
|
2878
2889
|
|
|
2890
|
+
// ── Terminal renderer init ─────────────────────────────────────
|
|
2891
|
+
terminal.setVersion(PKG_VERSION);
|
|
2892
|
+
terminal.init({ workers, startTime });
|
|
2893
|
+
terminal.startPhase(`Connecting ${accounts.length} accounts to Discord`);
|
|
2894
|
+
|
|
2879
2895
|
// ── Phase 1: Login accounts ─────────────────────────────────────────
|
|
2880
|
-
console.log(` ${rgb(52, 211, 153)}✓${c.reset} Logging in ${accounts.length} account(s)...`);
|
|
2881
2896
|
const parsedGapMin = Number.parseInt(String(process.env.LOGIN_GAP_MIN_MS || '50'), 10);
|
|
2882
2897
|
const parsedGapMax = Number.parseInt(String(process.env.LOGIN_GAP_MAX_MS || '150'), 10);
|
|
2883
2898
|
const LOGIN_GAP_MIN_MS = Number.isFinite(parsedGapMin) && parsedGapMin >= 0 ? parsedGapMin : 50;
|
|
@@ -2894,6 +2909,7 @@ async function start(apiKey, apiUrl, opts = {}) {
|
|
|
2894
2909
|
workers.push(worker);
|
|
2895
2910
|
workerMap.set(acc.id, worker);
|
|
2896
2911
|
await worker.start();
|
|
2912
|
+
terminal.updateProgress(workers.length, accounts.length);
|
|
2897
2913
|
}));
|
|
2898
2914
|
if (i + BATCH_SIZE < accounts.length) await new Promise(r => setTimeout(r, randomLoginGap()));
|
|
2899
2915
|
hintGC();
|
|
@@ -2902,8 +2918,9 @@ async function start(apiKey, apiUrl, opts = {}) {
|
|
|
2902
2918
|
const loginDone = workers.filter(w => !w._tokenInvalid && w.channel).length;
|
|
2903
2919
|
const invalidWorkers = workers.filter(w => w._tokenInvalid);
|
|
2904
2920
|
const timedOutWorkers = workers.filter(w => !w.channel && !w._tokenInvalid);
|
|
2905
|
-
|
|
2921
|
+
terminal.endPhase(`${loginDone}/${accounts.length} accounts connected`, true);
|
|
2906
2922
|
if (invalidWorkers.length > 0) {
|
|
2923
|
+
terminal.flashEvent('error', `${invalidWorkers.length} accounts have INVALID tokens`);
|
|
2907
2924
|
log('warn', `${rgb(239, 68, 68)}${invalidWorkers.length} account(s) have INVALID tokens`);
|
|
2908
2925
|
for (const w of invalidWorkers) log('error', ` ${w.account.label || w.account.id} — token invalid or expired`);
|
|
2909
2926
|
}
|
|
@@ -2912,24 +2929,31 @@ async function start(apiKey, apiUrl, opts = {}) {
|
|
|
2912
2929
|
const activeWorkers = workers.filter(w => !w._tokenInvalid);
|
|
2913
2930
|
|
|
2914
2931
|
// ── Phase 2: Inventory check ─────────────────────────────────────
|
|
2915
|
-
|
|
2932
|
+
terminal.startPhase('Checking inventory');
|
|
2933
|
+
terminal.updateProgress(0, activeWorkers.length);
|
|
2916
2934
|
let invDone = 0, invFailed = 0;
|
|
2917
2935
|
await Promise.all(activeWorkers.map(async (w) => {
|
|
2918
2936
|
try { await w.checkInventory({ force: true, requireComplete: true, maxAttempts: 3, silent: true }); }
|
|
2919
2937
|
catch { invFailed++; return; }
|
|
2920
2938
|
invDone++;
|
|
2939
|
+
terminal.updateProgress(invDone, activeWorkers.length);
|
|
2921
2940
|
}));
|
|
2922
2941
|
|
|
2923
2942
|
if (invFailed > 0) {
|
|
2943
|
+
terminal.endPhase(`Inventory: ${invFailed} failed`, false);
|
|
2924
2944
|
console.log(` ${rgb(239, 68, 68)}✗${c.reset} Inventory: ${invFailed} failed — not starting grind loops`);
|
|
2925
2945
|
return;
|
|
2926
2946
|
}
|
|
2927
|
-
|
|
2947
|
+
terminal.endPhase(`Inventory: ${invDone} clear`);
|
|
2928
2948
|
|
|
2929
2949
|
// ── Phase 2.5: Balance check ────────────────────────────────────
|
|
2930
|
-
|
|
2950
|
+
terminal.startPhase('Checking balance');
|
|
2951
|
+
terminal.updateProgress(0, activeWorkers.length);
|
|
2952
|
+
let balDone = 0;
|
|
2931
2953
|
await Promise.all(activeWorkers.map(async (w) => {
|
|
2932
2954
|
try { await w.checkBalance(true); } catch {}
|
|
2955
|
+
balDone++;
|
|
2956
|
+
terminal.updateProgress(balDone, activeWorkers.length);
|
|
2933
2957
|
}));
|
|
2934
2958
|
|
|
2935
2959
|
let totalWallet = 0, totalBank = 0, noLifesaverAccounts = [];
|
|
@@ -2938,13 +2962,16 @@ async function start(apiKey, apiUrl, opts = {}) {
|
|
|
2938
2962
|
totalBank += w.stats?.bankBalance || 0;
|
|
2939
2963
|
if (w._lifesavers === 0) noLifesaverAccounts.push(w.username || w.account.label);
|
|
2940
2964
|
}
|
|
2941
|
-
|
|
2965
|
+
terminal.endPhase(`Balance: ⏣ ${(totalWallet + totalBank).toLocaleString()}`);
|
|
2942
2966
|
if (noLifesaverAccounts.length > 0) {
|
|
2967
|
+
terminal.flashEvent('warn', `${noLifesaverAccounts.length} accounts have 0 LIFESAVERS`);
|
|
2943
2968
|
console.log(` ${rgb(239, 68, 68)}⚠${c.reset} ${noLifesaverAccounts.length} account(s) have 0 LIFESAVERS — crime/search disabled`);
|
|
2944
2969
|
}
|
|
2945
2970
|
|
|
2946
2971
|
// ── Phase 2.75: DM history check ────────────────────────────────
|
|
2947
|
-
|
|
2972
|
+
terminal.startPhase('Checking DM history');
|
|
2973
|
+
terminal.updateProgress(0, activeWorkers.length);
|
|
2974
|
+
let dmDone = 0;
|
|
2948
2975
|
let dmDeaths = 0, dmLevelUps = 0, dmNoLs = [], dmUnknown = [];
|
|
2949
2976
|
for (const w of activeWorkers) {
|
|
2950
2977
|
try {
|
|
@@ -2956,8 +2983,11 @@ async function start(apiKey, apiUrl, opts = {}) {
|
|
|
2956
2983
|
if (dm.currentLevel > 0) w._level = dm.currentLevel;
|
|
2957
2984
|
if (dm.lifesavers >= 0) w._lifesavers = dm.lifesavers;
|
|
2958
2985
|
} catch {}
|
|
2986
|
+
dmDone++;
|
|
2987
|
+
terminal.updateProgress(dmDone, activeWorkers.length);
|
|
2959
2988
|
}
|
|
2960
2989
|
if (dmNoLs.length > 0) {
|
|
2990
|
+
terminal.flashEvent('warn', `No lifesavers: ${dmNoLs.join(', ')}`);
|
|
2961
2991
|
log('warn', `⚠ No lifesavers: ${dmNoLs.join(', ')}`);
|
|
2962
2992
|
for (const w of activeWorkers) {
|
|
2963
2993
|
if (dmNoLs.includes(w.username) && redis) {
|
|
@@ -2975,17 +3005,16 @@ async function start(apiKey, apiUrl, opts = {}) {
|
|
|
2975
3005
|
if (dmDeaths > 0) dmSummaryParts.push(`${dmDeaths} deaths`);
|
|
2976
3006
|
if (dmLevelUps > 0) dmSummaryParts.push(`${dmLevelUps} level-ups`);
|
|
2977
3007
|
if (dmUnknown.length > 0) dmSummaryParts.push(`${dmUnknown.length} pending`);
|
|
2978
|
-
|
|
3008
|
+
terminal.endPhase(`DM check: ${dmSummaryParts.length > 0 ? dmSummaryParts.join(', ') : 'clean'}`);
|
|
2979
3009
|
|
|
2980
3010
|
// ── Phase 3: Start grind loops ───────────────────────────────────
|
|
2981
|
-
|
|
2982
|
-
// Phase 3: Start all grind loops (only for valid workers)
|
|
3011
|
+
terminal.startPhase(`🚀 Launching ${activeWorkers.length} grinders!`);
|
|
2983
3012
|
for (const w of activeWorkers) {
|
|
2984
3013
|
if (!shutdownCalled) w.grindLoop();
|
|
2985
3014
|
}
|
|
2986
|
-
|
|
2987
|
-
|
|
2988
|
-
|
|
3015
|
+
terminal.endPhase(`${activeWorkers.length} grinders active`);
|
|
3016
|
+
terminal.setWorkers(workers);
|
|
3017
|
+
terminal.setActive();
|
|
2989
3018
|
|
|
2990
3019
|
// Cluster heartbeat — lets other nodes see this node is alive
|
|
2991
3020
|
if (CLUSTER_ENABLED) {
|
|
@@ -3052,35 +3081,26 @@ async function start(apiKey, apiUrl, opts = {}) {
|
|
|
3052
3081
|
sigintHandled = true;
|
|
3053
3082
|
shutdownCalled = true;
|
|
3054
3083
|
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
3084
|
|
|
3063
|
-
// Collect stats
|
|
3064
|
-
let finalCoins = 0;
|
|
3065
|
-
let finalCmds = 0;
|
|
3085
|
+
// Collect stats for summary
|
|
3086
|
+
let finalCoins = 0, finalCmds = 0, totalSuccess = 0;
|
|
3066
3087
|
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
3088
|
finalCoins += wk.stats.coins || 0;
|
|
3075
3089
|
finalCmds += wk.stats.commands || 0;
|
|
3090
|
+
totalSuccess += wk.stats.successes || 0;
|
|
3076
3091
|
}
|
|
3077
|
-
console.log(sepBar);
|
|
3078
|
-
|
|
3079
3092
|
const memFinal = Math.round((process.memoryUsage?.rss?.() ?? process.memoryUsage().rss) / 1048576);
|
|
3080
|
-
const
|
|
3081
|
-
|
|
3082
|
-
|
|
3083
|
-
|
|
3093
|
+
const uptimeMs = Date.now() - startTime;
|
|
3094
|
+
|
|
3095
|
+
// Show beautiful session summary via terminal renderer
|
|
3096
|
+
terminal.shutdown({
|
|
3097
|
+
totalCoins: finalCoins,
|
|
3098
|
+
totalCmds: finalCmds,
|
|
3099
|
+
totalSuccess: finalCmds > 0 ? ((totalSuccess / finalCmds) * 100).toFixed(0) : 0,
|
|
3100
|
+
workers,
|
|
3101
|
+
uptime: uptimeMs,
|
|
3102
|
+
memMB: memFinal,
|
|
3103
|
+
});
|
|
3084
3104
|
|
|
3085
3105
|
// Release all cluster claims before stopping workers
|
|
3086
3106
|
const releasePromises = workers.map(wk => releaseClaim(wk.account.id).catch(() => {}));
|
|
@@ -3096,22 +3116,14 @@ async function start(apiKey, apiUrl, opts = {}) {
|
|
|
3096
3116
|
|
|
3097
3117
|
const totalRecoveries = workers.reduce((sum, wk) => sum + (wk._totalRecoveries || 0), 0);
|
|
3098
3118
|
const totalDisconnects = workers.reduce((sum, wk) => sum + (wk._disconnectCount || 0), 0);
|
|
3099
|
-
const totalRateLimits = workers.reduce((sum, wk) => sum + (wk._rateLimitHits || 0), 0);
|
|
3100
3119
|
|
|
3101
3120
|
const webhookMsg = `+⏣ ${finalCoins.toLocaleString()} | ${finalCmds} cmds | ${formatUptime()}` +
|
|
3102
3121
|
(totalRecoveries > 0 ? ` | ${totalRecoveries} auto-recoveries` : '') +
|
|
3103
3122
|
(CLUSTER_ENABLED ? ` | node: ${NODE_ID.substring(0, 12)}` : '');
|
|
3104
3123
|
sendWebhook('Session Ended', webhookMsg, 0x8b5cf6);
|
|
3105
3124
|
|
|
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
3125
|
if (redis) { try { redis.disconnect(); } catch {} }
|
|
3114
|
-
setTimeout(() => process.exit(0),
|
|
3126
|
+
setTimeout(() => process.exit(0), 500);
|
|
3115
3127
|
});
|
|
3116
3128
|
}
|
|
3117
3129
|
|
package/lib/terminal.js
ADDED
|
@@ -0,0 +1,820 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* terminal.js — Modern animated terminal renderer for DankGrinder
|
|
3
|
+
*
|
|
4
|
+
* Design goals:
|
|
5
|
+
* - Single-line-per-account rows (scales to 10k+ accounts)
|
|
6
|
+
* - Virtual window: only renders visible slice of accounts
|
|
7
|
+
* - 4 FPS render loop with dirty-row tracking (no flicker)
|
|
8
|
+
* - Graceful degradation: falls back to console.log if not a TTY
|
|
9
|
+
* - Graceful mode: Ctrl+C always restores cursor + shows summary
|
|
10
|
+
*
|
|
11
|
+
* Layout (fixed row heights, live-updated):
|
|
12
|
+
*
|
|
13
|
+
* ┌─ DANKGRINDER v7.77.0 ─────────────────────────────────────────────────────┐
|
|
14
|
+
* │ ⏱ 00:32 ⬡ 5 accounts ⏣ 47,230 ⚡ 12 cmd/m 📊 89% success │
|
|
15
|
+
* ├─ ACCOUNTS ──────────────────────────────────────────────────────────────────┤
|
|
16
|
+
* │ 💎 alice_99 ⏣ 12,450 L:24 ♥3 34cmds 🟢 grinding ⚔ adventure │
|
|
17
|
+
* │ 💎 bob_trades ⏣ 8,920 L:18 ♥5 28cmds 🟢 grinding 🐟 fishing │
|
|
18
|
+
* │ 💎 crypto_king ⏣ 5,100 L:31 ♥0 19cmds 🟡 depositing 💰 beg │
|
|
19
|
+
* │ 💎 diamond_h.. ⏣ 3,780 L:12 ♥8 14cmds 🟢 grinding 🌾 farm │
|
|
20
|
+
* │ 💎 moon_wallet ⏣ 1,200 L:9 ♥2 8cmds 🔴 paused ⚠ captcha │
|
|
21
|
+
* ├─ EVENTS ────────────────────────────────────────────────────────────────────┤
|
|
22
|
+
* │ 00:32 ⚔ alice_99 adventure: +⏣ 850 │
|
|
23
|
+
* │ 00:31 💀 crypto_king DEATH — 0 lifesavers! crime/search disabled │
|
|
24
|
+
* │ 00:30 ⬆️ bob_trades leveled up to Lv.19 │
|
|
25
|
+
* │ 00:28 🌾 diamond_hands farm: +⏣ 2,100 │
|
|
26
|
+
* └──────────────────────────────────────────────────────────────────────────────┘
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
const READY = (() => {
|
|
30
|
+
try {
|
|
31
|
+
return process.stdout.isTTY && !process.env.NO_TERM;
|
|
32
|
+
} catch (_) { return false; }
|
|
33
|
+
})();
|
|
34
|
+
|
|
35
|
+
// ── ANSI Helpers ────────────────────────────────────────────────────────────
|
|
36
|
+
|
|
37
|
+
const A = {
|
|
38
|
+
reset: '\x1b[0m',
|
|
39
|
+
bold: '\x1b[1m',
|
|
40
|
+
dim: '\x1b[2m',
|
|
41
|
+
italic: '\x1b[3m',
|
|
42
|
+
|
|
43
|
+
black: '\x1b[30m',
|
|
44
|
+
red: '\x1b[31m',
|
|
45
|
+
green: '\x1b[32m',
|
|
46
|
+
yellow: '\x1b[33m',
|
|
47
|
+
blue: '\x1b[34m',
|
|
48
|
+
magenta: '\x1b[35m',
|
|
49
|
+
cyan: '\x1b[36m',
|
|
50
|
+
white: '\x1b[37m',
|
|
51
|
+
|
|
52
|
+
bgBlack: '\x1b[40m',
|
|
53
|
+
bgRed: '\x1b[41m',
|
|
54
|
+
bgGreen: '\x1b[42m',
|
|
55
|
+
bgYellow: '\x1b[43m',
|
|
56
|
+
bgBlue: '\x1b[44m',
|
|
57
|
+
bgMagenta: '\x1b[45m',
|
|
58
|
+
bgCyan: '\x1b[46m',
|
|
59
|
+
bgWhite: '\x1b[47m',
|
|
60
|
+
|
|
61
|
+
// 256-color RGB shortcuts
|
|
62
|
+
rgb: (r, g, b) => `\x1b[38;2;${r};${g};${b}m`,
|
|
63
|
+
|
|
64
|
+
// Cursor
|
|
65
|
+
save: '\x1b7',
|
|
66
|
+
restore: '\x1b8',
|
|
67
|
+
hide: '\x1b[?25l',
|
|
68
|
+
show: '\x1b[?25h',
|
|
69
|
+
up: (n = 1) => `\x1b[${n}A`,
|
|
70
|
+
down: (n = 1) => `\x1b[${n}B`,
|
|
71
|
+
right: (n = 1) => `\x1b[${n}C`,
|
|
72
|
+
left: (n = 1) => `\x1b[${n}D`,
|
|
73
|
+
col: (n) => `\x1b[${n}G`,
|
|
74
|
+
clear: '\x1b[2J',
|
|
75
|
+
clearLine: '\x1b[2K',
|
|
76
|
+
home: '\x1b[H',
|
|
77
|
+
|
|
78
|
+
// 256-color palette
|
|
79
|
+
purple: '\x1b[38;5;141m', // #8b5cf6
|
|
80
|
+
pink: '\x1b[38;5;205m', // #ff5c93
|
|
81
|
+
orange: '\x1b[38;5;214m', // #ff9f43
|
|
82
|
+
teal: '\x1b[38;5;44m', // #2dd4bf
|
|
83
|
+
lime: '\x1b[38;5;82m', // #4cd137
|
|
84
|
+
crimson: '\x1b[38;5;196m', // #ff4757
|
|
85
|
+
slate: '\x1b[38;5;245m', // #a0a0b0
|
|
86
|
+
gold: '\x1b[38;5;220m', // #ffc233
|
|
87
|
+
emerald: '\x1b[38;5;48m', // #2ed573
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
// ── Color scheme ────────────────────────────────────────────────────────────
|
|
91
|
+
|
|
92
|
+
const C = {
|
|
93
|
+
header: A.purple,
|
|
94
|
+
headerDim: A.rgb(100, 70, 180),
|
|
95
|
+
border: A.rgb(60, 50, 90),
|
|
96
|
+
borderDim: A.rgb(40, 35, 60),
|
|
97
|
+
|
|
98
|
+
rowDefault: A.white,
|
|
99
|
+
rowAlt: A.rgb(30, 28, 45),
|
|
100
|
+
rowHighlight: A.rgb(25, 23, 40),
|
|
101
|
+
|
|
102
|
+
name: A.rgb(255, 255, 255),
|
|
103
|
+
nameDim: A.slate,
|
|
104
|
+
|
|
105
|
+
coins: A.gold,
|
|
106
|
+
level: A.cyan,
|
|
107
|
+
lifesavers: A.pink,
|
|
108
|
+
lifesaversLow: A.crimson,
|
|
109
|
+
lifesaversMid: A.orange,
|
|
110
|
+
|
|
111
|
+
statusActive: A.emerald, // 🟢 grinding
|
|
112
|
+
statusPaused: A.crimson, // 🔴 paused
|
|
113
|
+
statusWarning: A.orange, // 🟡 warning
|
|
114
|
+
statusOffline: A.slate, // ⚫ offline
|
|
115
|
+
statusConnecting: A.yellow, // 🟡 connecting
|
|
116
|
+
|
|
117
|
+
cmdSuccess: A.emerald,
|
|
118
|
+
cmdError: A.crimson,
|
|
119
|
+
cmdEvent: A.purple,
|
|
120
|
+
cmdWarn: A.orange,
|
|
121
|
+
|
|
122
|
+
statLabel: A.slate,
|
|
123
|
+
statValue: A.white,
|
|
124
|
+
|
|
125
|
+
headerBg: A.rgb(20, 15, 35),
|
|
126
|
+
rowBg1: '',
|
|
127
|
+
rowBg2: A.rgb(25, 22, 38),
|
|
128
|
+
rowBgPaused: A.rgb(40, 15, 20),
|
|
129
|
+
rowBgWarning: A.rgb(40, 35, 15),
|
|
130
|
+
rowBgDeath: A.rgb(50, 15, 15),
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
// ── Spinner frames ──────────────────────────────────────────────────────────
|
|
134
|
+
|
|
135
|
+
const SPINNERS = {
|
|
136
|
+
dots: ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠿'],
|
|
137
|
+
moon: ['🌑', '🌒', '🌓', '🌔', '🌕', '🌖', '🌗', '🌘'],
|
|
138
|
+
arrows: ['←', '↖', '↑', '↗', '→', '↘', '↓', '↙'],
|
|
139
|
+
pulse: ['▓', '▒', '░', '▒'],
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
// ── Terminal Renderer ────────────────────────────────────────────────────────
|
|
143
|
+
|
|
144
|
+
class Terminal {
|
|
145
|
+
constructor() {
|
|
146
|
+
this.workers = [];
|
|
147
|
+
this.events = []; // recent event feed (last N events)
|
|
148
|
+
this.MAX_EVENTS = 5;
|
|
149
|
+
|
|
150
|
+
this.phaseName = '';
|
|
151
|
+
this.phaseFrame = 0;
|
|
152
|
+
this.phaseTimer = null;
|
|
153
|
+
|
|
154
|
+
this.dirtyWorkers = new Set();
|
|
155
|
+
this.dirtyEvents = false;
|
|
156
|
+
this.dirtyStats = true;
|
|
157
|
+
this._renderTimer = null;
|
|
158
|
+
this._startTime = 0;
|
|
159
|
+
|
|
160
|
+
// Virtual window state
|
|
161
|
+
this.windowStart = 0;
|
|
162
|
+
this.windowSize = 10;
|
|
163
|
+
this.scrollLocked = false; // true = auto-follow most active worker
|
|
164
|
+
|
|
165
|
+
this._lineCount = 0;
|
|
166
|
+
this._active = false;
|
|
167
|
+
this._shutdown = false;
|
|
168
|
+
|
|
169
|
+
// Row offsets (set during render)
|
|
170
|
+
this._headerRow = 0;
|
|
171
|
+
this._statsRow = 0;
|
|
172
|
+
this._accountsRow = 0;
|
|
173
|
+
this._eventsRow = 0;
|
|
174
|
+
this._footerRow = 0;
|
|
175
|
+
|
|
176
|
+
// ── Scroll position state ──
|
|
177
|
+
this._followWorkerIdx = -1; // -1 = follow newest active worker
|
|
178
|
+
|
|
179
|
+
this._w = 80;
|
|
180
|
+
this._h = 24;
|
|
181
|
+
this._resizeTimer = null;
|
|
182
|
+
this._col = (n) => `\x1b[${n}G`;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// ── Public API ──────────────────────────────────────────────────────────
|
|
186
|
+
|
|
187
|
+
init(opts = {}) {
|
|
188
|
+
if (opts.startTime) this._startTime = opts.startTime;
|
|
189
|
+
this.workers = opts.workers || [];
|
|
190
|
+
this._updateSize();
|
|
191
|
+
|
|
192
|
+
if (READY) {
|
|
193
|
+
process.stdout.write(A.hide);
|
|
194
|
+
process.stdout.on('resize', this._onResize.bind(this));
|
|
195
|
+
this._drawStartupScreen();
|
|
196
|
+
} else {
|
|
197
|
+
console.log(`${C.header}🚀 DankGrinder starting...${A.reset}`);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
setVersion(v) { this._version = v; }
|
|
202
|
+
|
|
203
|
+
startPhase(name) {
|
|
204
|
+
this.phaseName = name;
|
|
205
|
+
this.phaseFrame = 0;
|
|
206
|
+
if (!READY) {
|
|
207
|
+
console.log(` ${A.dim}⟳${A.reset} ${name}...`);
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
if (this.phaseTimer) clearInterval(this.phaseTimer);
|
|
211
|
+
this.phaseTimer = setInterval(() => {
|
|
212
|
+
this.phaseFrame = (this.phaseFrame + 1) % SPINNERS.dots.length;
|
|
213
|
+
this._redrawPhaseSpinner();
|
|
214
|
+
}, 120);
|
|
215
|
+
this._redrawPhaseSpinner();
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
updateProgress(done, total) {
|
|
219
|
+
if (!READY) return;
|
|
220
|
+
this._writePhaseProgress(done, total);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
endPhase(name, ok = true) {
|
|
224
|
+
if (this.phaseTimer) { clearInterval(this.phaseTimer); this.phaseTimer = null; }
|
|
225
|
+
if (!READY) {
|
|
226
|
+
const icon = ok ? `${C.header}✓${A.reset}` : `${A.crimson}✗${A.reset}`;
|
|
227
|
+
console.log(` ${icon} ${name}`);
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
const icon = ok ? `${C.header}✓${A.reset}` : `${A.crimson}✗${A.reset}`;
|
|
231
|
+
const frame = SPINNERS.dots[0];
|
|
232
|
+
const cols = this._w;
|
|
233
|
+
const label = ` ${frame} ${name}`;
|
|
234
|
+
const pad = cols - this._ansiLen(label) - 4;
|
|
235
|
+
process.stdout.write(
|
|
236
|
+
A.save +
|
|
237
|
+
this._cursor(2, 1) +
|
|
238
|
+
A.clearLine +
|
|
239
|
+
`${A.save}${A.home}` +
|
|
240
|
+
`${label}${pad > 0 ? ' '.repeat(pad) : ''}` +
|
|
241
|
+
`${this._rpad('', cols - this._ansiLen(` ${icon} ${name}`) - 2)}${icon} ${name}` +
|
|
242
|
+
A.restore
|
|
243
|
+
);
|
|
244
|
+
// Clear spinner line
|
|
245
|
+
process.stdout.write(this._cursor(3, 1) + A.clearLine);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
flashEvent(type, msg) {
|
|
249
|
+
const now = new Date();
|
|
250
|
+
const ts = `${A.dim}${String(now.getMinutes()).padStart(2, '0')}:${String(now.getSeconds()).padStart(2, '0')}${A.reset}`;
|
|
251
|
+
this.events.unshift({ ts, type, msg, id: Date.now() });
|
|
252
|
+
if (this.events.length > this.MAX_EVENTS) this.events.pop();
|
|
253
|
+
this.dirtyEvents = true;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
setWorkers(workers) {
|
|
257
|
+
this.workers = workers;
|
|
258
|
+
this.dirtyWorkers = new Set(workers.map((_, i) => i));
|
|
259
|
+
this.dirtyStats = true;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
markWorkerDirty(idx) {
|
|
263
|
+
this.dirtyWorkers.add(idx);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
setActive() {
|
|
267
|
+
if (this._active) return;
|
|
268
|
+
this._active = true;
|
|
269
|
+
if (!READY) return;
|
|
270
|
+
|
|
271
|
+
// Clear startup, draw live view
|
|
272
|
+
process.stdout.write(A.clear + A.home);
|
|
273
|
+
this._drawLiveView();
|
|
274
|
+
this._startRenderLoop();
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
scrollBy(delta) {
|
|
278
|
+
if (!READY || this._shutdown) return;
|
|
279
|
+
const max = Math.max(0, this.workers.length - this.windowSize);
|
|
280
|
+
this.windowStart = Math.max(0, Math.min(max, this.windowStart + delta));
|
|
281
|
+
this._followWorkerIdx = -1; // manual scroll cancels auto-follow
|
|
282
|
+
this.dirtyWorkers = new Set();
|
|
283
|
+
for (let i = this.windowStart; i < this.windowStart + this.windowSize; i++) {
|
|
284
|
+
if (i < this.workers.length) this.dirtyWorkers.add(i);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
followWorker(idx) {
|
|
289
|
+
this._followWorkerIdx = idx;
|
|
290
|
+
this.scrollLocked = true;
|
|
291
|
+
// Ensure the worker is visible
|
|
292
|
+
if (idx < this.windowStart) {
|
|
293
|
+
this.windowStart = idx;
|
|
294
|
+
} else if (idx >= this.windowStart + this.windowSize) {
|
|
295
|
+
this.windowStart = idx - this.windowSize + 1;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
shutdown(summary = {}) {
|
|
300
|
+
this._shutdown = true;
|
|
301
|
+
if (this._renderTimer) { clearInterval(this._renderTimer); this._renderTimer = null; }
|
|
302
|
+
if (this._phaseTimer) { clearInterval(this._phaseTimer); this._phaseTimer = null; }
|
|
303
|
+
|
|
304
|
+
process.stdout.write(A.show);
|
|
305
|
+
|
|
306
|
+
if (!READY) {
|
|
307
|
+
this._printSummaryPlain(summary);
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Move to clean area below any existing output
|
|
312
|
+
const cols = this._w;
|
|
313
|
+
const { totalCoins = 0, totalCmds = 0, totalSuccess = 0,
|
|
314
|
+
workers = [], uptime = 0, memMB = 0 } = summary;
|
|
315
|
+
|
|
316
|
+
const b = C.border;
|
|
317
|
+
const h = C.header;
|
|
318
|
+
const g = C.statValue;
|
|
319
|
+
const dim = A.dim;
|
|
320
|
+
const r = A.reset;
|
|
321
|
+
|
|
322
|
+
const sep = (c) => `${b}${c.repeat(cols - 2)}${r}`;
|
|
323
|
+
const bar = `${b}${'─'.repeat(cols - 2)}${r}`;
|
|
324
|
+
const icon = (e) => `${h}${e}${r}`;
|
|
325
|
+
|
|
326
|
+
let out = '';
|
|
327
|
+
out += `${A.clear}${A.home}`;
|
|
328
|
+
out += `${A.save}`;
|
|
329
|
+
|
|
330
|
+
// Box title
|
|
331
|
+
out += `${this._at(1, 1)}${bar}`;
|
|
332
|
+
out += `${this._at(2, 1)}${b} ${h}${A.bold}⬡ DANKGRINDER — Session Summary${r} ${b}${'─'.repeat(Math.max(0, cols - 37))}${r}`;
|
|
333
|
+
out += `${this._at(3, 1)}${bar}`;
|
|
334
|
+
|
|
335
|
+
// Per-account summary
|
|
336
|
+
let row = 4;
|
|
337
|
+
for (const wk of workers) {
|
|
338
|
+
const coins = `+⏣ ${(wk.stats?.coins || 0).toLocaleString()}`;
|
|
339
|
+
const cmds = `${wk.stats?.commands || 0}cmds`;
|
|
340
|
+
const rate = wk.stats?.commands > 0
|
|
341
|
+
? `${((wk.stats.successes / wk.stats.commands) * 100).toFixed(0)}%`
|
|
342
|
+
: '0%';
|
|
343
|
+
const ls = wk._lifesavers ?? '?';
|
|
344
|
+
const lv = wk._level ?? '?';
|
|
345
|
+
const line = ` ${icon('💎')} ${g}${A.bold}${(wk.username || '?').padEnd(18)}${r} ${C.coins}${coins}${r} ${dim}${cmds}${r} ${g}${rate} OK${r} ${C.level}Lv.${lv}${r} ${C.lifesavers}♥${ls}${r}`;
|
|
346
|
+
out += `${this._at(row++, 1)}${b}${this._rpad(line, cols - 2)}${r}`;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
row++; // blank row
|
|
350
|
+
out += `${this._at(row++, 1)}${bar}`;
|
|
351
|
+
|
|
352
|
+
// Totals
|
|
353
|
+
const totalLine = ` ${icon('💰')} ${h}${A.bold}Total:${r} ${C.coins}${A.bold}⏣ ${totalCoins.toLocaleString()}${r} ${dim}${totalCmds} cmds${r} ${g}${totalSuccess}% OK${r} ${dim}${this._fmtUptime(uptime)}${r} ${dim}${memMB}MB${r}`;
|
|
354
|
+
out += `${this._at(row++, 1)}${b}${this._rpad(totalLine, cols - 2)}${r}`;
|
|
355
|
+
out += `${this._at(row++, 1)}${sep('─')}`;
|
|
356
|
+
out += `${A.restore}`;
|
|
357
|
+
out += `${A.show}`;
|
|
358
|
+
|
|
359
|
+
process.stdout.write(out);
|
|
360
|
+
setTimeout(() => {}, 100);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// ── Internal ────────────────────────────────────────────────────────────
|
|
364
|
+
|
|
365
|
+
_updateSize() {
|
|
366
|
+
try {
|
|
367
|
+
this._w = process.stdout.columns || 80;
|
|
368
|
+
this._h = process.stdout.rows || 24;
|
|
369
|
+
// Window size: leave room for header (3 rows) + stats (2) + events footer (3) + border (2)
|
|
370
|
+
this.windowSize = Math.max(3, this._h - 12);
|
|
371
|
+
} catch (_) {
|
|
372
|
+
this._w = 80; this._h = 24; this.windowSize = 10;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
_onResize() {
|
|
377
|
+
if (this._resizeTimer) clearTimeout(this._resizeTimer);
|
|
378
|
+
this._resizeTimer = setTimeout(() => {
|
|
379
|
+
this._updateSize();
|
|
380
|
+
if (this._active) {
|
|
381
|
+
this._clearScreen();
|
|
382
|
+
this._drawLiveView();
|
|
383
|
+
}
|
|
384
|
+
}, 100);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
_ansiLen(s) {
|
|
388
|
+
// Fast ANSI strip — just count escape sequences by looking for \x1b[
|
|
389
|
+
let len = 0;
|
|
390
|
+
let i = 0;
|
|
391
|
+
const str = String(s);
|
|
392
|
+
while (i < str.length) {
|
|
393
|
+
if (str.charCodeAt(i) === 0x1b && str[i + 1] === '[') {
|
|
394
|
+
let j = i + 2;
|
|
395
|
+
while (j < str.length && str[j] !== 'm') j++;
|
|
396
|
+
i = j + 1;
|
|
397
|
+
} else {
|
|
398
|
+
len++;
|
|
399
|
+
i++;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
return len;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
_rpad(s, width) {
|
|
406
|
+
const len = this._ansiLen(s);
|
|
407
|
+
const pad = width > len ? width - len : 0;
|
|
408
|
+
return s + (pad > 0 ? ' '.repeat(pad) : '');
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
_cursor(row, col) {
|
|
412
|
+
// Position cursor at absolute row/col (1-indexed)
|
|
413
|
+
return `\x1b[${row};${col}H`;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
_at(row, col) {
|
|
417
|
+
return `\x1b[${row};${col}H`;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
_write(str) {
|
|
421
|
+
process.stdout.write(str);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
_ansi(s, code) { return `${code}${s}${A.reset}`; }
|
|
425
|
+
_bold(s) { return `${A.bold}${s}${A.reset}`; }
|
|
426
|
+
_dim(s) { return `${A.dim}${s}${A.reset}`; }
|
|
427
|
+
|
|
428
|
+
_fmtUptime(ms) {
|
|
429
|
+
if (!ms) return '0s';
|
|
430
|
+
const s = Math.floor(ms / 1000);
|
|
431
|
+
if (s < 60) return `${s}s`;
|
|
432
|
+
const m = Math.floor(s / 60);
|
|
433
|
+
if (m < 60) return `${m}m ${s % 60}s`;
|
|
434
|
+
const h = Math.floor(m / 60);
|
|
435
|
+
if (h < 24) return `${h}h ${m % 60}m`;
|
|
436
|
+
const d = Math.floor(h / 24);
|
|
437
|
+
return `${d}d ${h % 24}h`;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
_fmtCoins(n) {
|
|
441
|
+
if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`;
|
|
442
|
+
if (n >= 1_000) return `${(n / 1_000).toFixed(1)}K`;
|
|
443
|
+
return String(n);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
_buildAccountRow(wk, idx) {
|
|
447
|
+
const w = this._w;
|
|
448
|
+
// Row layout (all in one line):
|
|
449
|
+
// #N 💎 username ⏣coins Lv.N ♥N Ncmds ● status ⚔ current_cmd
|
|
450
|
+
// We need to fit within `w` columns, truncate username if needed
|
|
451
|
+
|
|
452
|
+
const num = `${idx + 1}.`.padEnd(3);
|
|
453
|
+
const username = (wk.username || '?').substring(0, 16).padEnd(17);
|
|
454
|
+
const coins = `⏣${this._fmtCoins(wk.stats?.coins || 0)}`.padEnd(8);
|
|
455
|
+
const level = `Lv.${wk._level ?? '?'}`.padEnd(5);
|
|
456
|
+
const ls = wk._lifesavers ?? '?';
|
|
457
|
+
const lifesaversColor = ls === 0 ? C.lifesaversLow
|
|
458
|
+
: ls <= 2 ? C.lifesaversMid
|
|
459
|
+
: C.lifesavers;
|
|
460
|
+
const lifesavers = `${lifesaversColor}♥${ls}`.padEnd(4);
|
|
461
|
+
const cmds = `${wk.stats?.commands || 0}cmds`.padEnd(7);
|
|
462
|
+
|
|
463
|
+
// Status dot + text
|
|
464
|
+
let statusDot, statusText, rowBg;
|
|
465
|
+
if (!wk.running || wk._tokenInvalid) {
|
|
466
|
+
statusDot = '⚫'; statusText = 'offline'; rowBg = C.rowBgPaused;
|
|
467
|
+
} else if (wk.paused) {
|
|
468
|
+
statusDot = '🔴'; statusText = 'paused'; rowBg = C.rowBgPaused;
|
|
469
|
+
} else if (wk.dashboardPaused) {
|
|
470
|
+
statusDot = '🟠'; statusText = 'dashboard'; rowBg = C.rowBgWarning;
|
|
471
|
+
} else {
|
|
472
|
+
statusDot = '🟢'; statusText = 'grinding'; rowBg = '';
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// Current command
|
|
476
|
+
const cmd = (wk.lastStatus || '').substring(0, 20).padEnd(21);
|
|
477
|
+
const successRate = wk.stats?.commands > 0
|
|
478
|
+
? `${((wk.stats.successes / wk.stats.commands) * 100).toFixed(0)}%`
|
|
479
|
+
: '0%';
|
|
480
|
+
|
|
481
|
+
const parts = [
|
|
482
|
+
`${C.header}${num}${A.reset}`,
|
|
483
|
+
`${C.name}${username}${A.reset}`,
|
|
484
|
+
`${C.coins}${coins}${A.reset}`,
|
|
485
|
+
`${C.level}${level}${A.reset}`,
|
|
486
|
+
`${lifesaversColor}${lifesavers}${A.reset}`,
|
|
487
|
+
`${C.statValue}${cmds}${A.reset}`,
|
|
488
|
+
`${C.statValue}${successRate}`.padEnd(5) + A.reset,
|
|
489
|
+
`${statusDot} ${statusText}`.padEnd(14),
|
|
490
|
+
`${this._dim(cmd)}`,
|
|
491
|
+
];
|
|
492
|
+
|
|
493
|
+
// Join and pad to screen width
|
|
494
|
+
const line = parts.join(' ');
|
|
495
|
+
return this._rpad(line, w);
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
_drawStartupScreen() {
|
|
499
|
+
const w = this._w;
|
|
500
|
+
const b = C.border;
|
|
501
|
+
const h = C.header;
|
|
502
|
+
const g = C.statValue;
|
|
503
|
+
const dim = A.dim;
|
|
504
|
+
const r = A.reset;
|
|
505
|
+
|
|
506
|
+
const sep = (c) => `${b}${c.repeat(w - 2)}${r}`;
|
|
507
|
+
|
|
508
|
+
// Figure out how many rows we need for the spinner
|
|
509
|
+
const spinnerRow = 3;
|
|
510
|
+
const progressRow = 5;
|
|
511
|
+
const readyRow = 7;
|
|
512
|
+
|
|
513
|
+
let out = '';
|
|
514
|
+
out += `${A.clear}${A.home}`;
|
|
515
|
+
|
|
516
|
+
// Title box
|
|
517
|
+
const titleText = ` ⬡ DANKGRINDER v${this._version || '?'} `;
|
|
518
|
+
const titlePad = w - 2 - this._ansiLen(titleText);
|
|
519
|
+
out += `${this._at(1, 1)}${sep('─')}`;
|
|
520
|
+
out += `${this._at(2, 1)}${b} ${h}${A.bold}${titleText}${r}${b}${'─'.repeat(Math.max(0, titlePad))}${r}`;
|
|
521
|
+
out += `${this._at(3, 1)}${sep('─')}`;
|
|
522
|
+
|
|
523
|
+
// Spinner + phase text
|
|
524
|
+
out += `${this._at(5, 1)}${b}${' '.repeat(w - 2)}${r}`;
|
|
525
|
+
out += `${this._at(6, 1)}${b}${' '.repeat(w - 2)}${r}`;
|
|
526
|
+
|
|
527
|
+
// Footer hint
|
|
528
|
+
out += `${this._at(8, 1)}${sep('─')}`;
|
|
529
|
+
const hint = `${dim}Starting up...${r}`;
|
|
530
|
+
out += `${this._at(9, 1)}${b} ${hint}${' '.repeat(Math.max(0, w - 2 - this._ansiLen(hint)))}${r}`;
|
|
531
|
+
out += `${this._at(10, 1)}${sep('─')}`;
|
|
532
|
+
|
|
533
|
+
this._lineCount = 10;
|
|
534
|
+
this._write(out);
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
_redrawPhaseSpinner() {
|
|
538
|
+
if (!READY) return;
|
|
539
|
+
const frame = SPINNERS.dots[this.phaseFrame];
|
|
540
|
+
const label = this.phaseName;
|
|
541
|
+
const w = this._w;
|
|
542
|
+
|
|
543
|
+
const line = ` ${frame} ${label}...`;
|
|
544
|
+
const pad = w - this._ansiLen(line) - 2;
|
|
545
|
+
const padded = line + (pad > 0 ? ' '.repeat(pad) : '');
|
|
546
|
+
|
|
547
|
+
const b = C.border;
|
|
548
|
+
const r = A.reset;
|
|
549
|
+
this._write(
|
|
550
|
+
`${A.save}` +
|
|
551
|
+
`${this._cursor(6, 1)}` +
|
|
552
|
+
`${b}${padded}${r}` +
|
|
553
|
+
`${A.restore}`
|
|
554
|
+
);
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
_writePhaseProgress(done, total) {
|
|
558
|
+
if (!READY) {
|
|
559
|
+
console.log(` ${A.dim} → ${done}/${total}${A.reset}`);
|
|
560
|
+
return;
|
|
561
|
+
}
|
|
562
|
+
const w = this._w;
|
|
563
|
+
const b = C.border;
|
|
564
|
+
const h = C.header;
|
|
565
|
+
const dim = A.dim;
|
|
566
|
+
const r = A.reset;
|
|
567
|
+
const barLen = Math.max(10, w - 30);
|
|
568
|
+
const filled = Math.round((done / total) * barLen);
|
|
569
|
+
const empty = barLen - filled;
|
|
570
|
+
|
|
571
|
+
const bar = `${h}${'█'.repeat(filled)}${dim}${'░'.repeat(empty)}${r}`;
|
|
572
|
+
const label = ` ${done}/${total} `;
|
|
573
|
+
const line = `${bar} ${label}`;
|
|
574
|
+
const pad = w - this._ansiLen(line) - 2;
|
|
575
|
+
|
|
576
|
+
this._write(
|
|
577
|
+
`${A.save}` +
|
|
578
|
+
`${this._cursor(6, 1)}` +
|
|
579
|
+
`${b}${line}${' '.repeat(Math.max(0, pad))}${r}` +
|
|
580
|
+
`${A.restore}`
|
|
581
|
+
);
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
_clearScreen() {
|
|
585
|
+
this._write(`${A.clear}${A.home}`);
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
_drawLiveView() {
|
|
589
|
+
if (!READY) return;
|
|
590
|
+
this._drawHeader();
|
|
591
|
+
this._drawAccounts();
|
|
592
|
+
this._drawEvents();
|
|
593
|
+
this._drawFooter();
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
_drawHeader() {
|
|
597
|
+
const w = this._w;
|
|
598
|
+
const b = C.border;
|
|
599
|
+
const h = C.header;
|
|
600
|
+
const g = C.statValue;
|
|
601
|
+
const dim = A.dim;
|
|
602
|
+
const r = A.reset;
|
|
603
|
+
|
|
604
|
+
const sep = `${b}${'─'.repeat(w - 2)}${r}`;
|
|
605
|
+
const title = ` ⬡ DANKGRINDER v${this._version || '?'} `;
|
|
606
|
+
const titlePad = w - 2 - this._ansiLen(title);
|
|
607
|
+
|
|
608
|
+
this._headerRow = 1;
|
|
609
|
+
this._write(`${this._at(1, 1)}${sep}`);
|
|
610
|
+
this._write(`${this._at(2, 1)}${b} ${h}${A.bold}${title}${r}${b}${'─'.repeat(Math.max(0, titlePad))}${r}`);
|
|
611
|
+
this._write(`${this._at(3, 1)}${sep}`);
|
|
612
|
+
this._statsRow = 4;
|
|
613
|
+
|
|
614
|
+
// Stats bar
|
|
615
|
+
const stats = this._buildStatsLine();
|
|
616
|
+
this._write(`${this._at(4, 1)}${b}${this._rpad(' ' + stats, w - 2)}${r}`);
|
|
617
|
+
this._accountsRow = 5;
|
|
618
|
+
this._write(`${this._at(5, 1)}${sep}`);
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
_buildStatsLine() {
|
|
622
|
+
const g = C.statValue;
|
|
623
|
+
const dim = A.dim;
|
|
624
|
+
const h = C.header;
|
|
625
|
+
const coins = C.coins;
|
|
626
|
+
|
|
627
|
+
let totalCoins = 0, totalCmds = 0, totalSuccess = 0, totalLs = 0;
|
|
628
|
+
let pausedCount = 0, activeCount = 0;
|
|
629
|
+
|
|
630
|
+
for (const wk of this.workers) {
|
|
631
|
+
totalCoins += wk.stats?.coins || 0;
|
|
632
|
+
totalCmds += wk.stats?.commands || 0;
|
|
633
|
+
totalSuccess += wk.stats?.successes || 0;
|
|
634
|
+
if (wk._lifesavers != null) totalLs += wk._lifesavers;
|
|
635
|
+
if (!wk.running || wk._tokenInvalid) {}
|
|
636
|
+
else if (wk.paused || wk.dashboardPaused) pausedCount++;
|
|
637
|
+
else activeCount++;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
const uptime = this._fmtUptime(Date.now() - (this._startTime || Date.now()));
|
|
641
|
+
const rate = totalCmds > 0 ? ((totalSuccess / totalCmds) * 100).toFixed(0) : 0;
|
|
642
|
+
|
|
643
|
+
return [
|
|
644
|
+
`${dim}⏱${A.reset} ${g}${uptime}${A.reset}`,
|
|
645
|
+
`${dim}⬡${A.reset} ${g}${this.workers.length}${A.reset} ${dim}accounts${A.reset}`,
|
|
646
|
+
`${coins}⏣${A.reset} ${g}${totalCoins.toLocaleString()}${A.reset}`,
|
|
647
|
+
`${dim}⚡${A.reset} ${g}${totalCmds}${A.reset} ${dim}cmds${A.reset}`,
|
|
648
|
+
`${dim}📊${A.reset} ${g}${rate}%${A.reset}`,
|
|
649
|
+
`${C.lifesavers}♥${A.reset} ${g}${totalLs}${A.reset}`,
|
|
650
|
+
`${h}🟢${A.reset} ${g}${activeCount}${A.reset} ${h}🔴${A.reset} ${g}${pausedCount}${A.reset}`,
|
|
651
|
+
].join(' ');
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
_drawAccounts() {
|
|
655
|
+
const w = this._w;
|
|
656
|
+
const b = C.border;
|
|
657
|
+
const dim = A.dim;
|
|
658
|
+
const r = A.reset;
|
|
659
|
+
const sep = `${b}${'─'.repeat(w - 2)}${r}`;
|
|
660
|
+
|
|
661
|
+
const visible = this.workers.slice(this.windowStart, this.windowStart + this.windowSize);
|
|
662
|
+
|
|
663
|
+
// Column header
|
|
664
|
+
const cols = [
|
|
665
|
+
`${C.header}#${A.reset}`,
|
|
666
|
+
`${C.header}ACCOUNT${A.reset}`,
|
|
667
|
+
`${C.header}COINS${A.reset}`,
|
|
668
|
+
`${C.header}LV${A.reset}`,
|
|
669
|
+
`${C.header}♥${A.reset}`,
|
|
670
|
+
`${C.header}CMDS${A.reset}`,
|
|
671
|
+
`${C.header}OK%${A.reset}`,
|
|
672
|
+
`${C.header}STATUS${A.reset}`,
|
|
673
|
+
`${C.header}CURRENT${A.reset}`,
|
|
674
|
+
].join(' ');
|
|
675
|
+
this._write(`${this._at(this._accountsRow, 1)}${b}${this._rpad(' ' + cols, w - 2)}${r}`);
|
|
676
|
+
this._write(`${this._at(this._accountsRow + 1, 1)}${sep}`);
|
|
677
|
+
|
|
678
|
+
// Worker rows
|
|
679
|
+
for (let i = 0; i < this.windowSize; i++) {
|
|
680
|
+
const row = this._accountsRow + 2 + i;
|
|
681
|
+
if (row > this._h - 4) break;
|
|
682
|
+
|
|
683
|
+
if (i < visible.length) {
|
|
684
|
+
const wk = visible[i];
|
|
685
|
+
const line = this._buildAccountRow(wk, this.windowStart + i);
|
|
686
|
+
this._write(`${this._at(row, 1)}${b}${line}${r}`);
|
|
687
|
+
} else {
|
|
688
|
+
this._write(`${this._at(row, 1)}${b}${' '.repeat(w - 2)}${r}`);
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
this._eventsRow = this._accountsRow + 2 + this.windowSize + 1;
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
_drawEvents() {
|
|
695
|
+
const w = this._w;
|
|
696
|
+
const b = C.border;
|
|
697
|
+
const dim = A.dim;
|
|
698
|
+
const r = A.reset;
|
|
699
|
+
const sep = `${b}${'─'.repeat(w - 2)}${r}`;
|
|
700
|
+
|
|
701
|
+
if (this._eventsRow > this._h - 4) return;
|
|
702
|
+
this._write(`${this._at(this._eventsRow, 1)}${sep}`);
|
|
703
|
+
|
|
704
|
+
const visibleEvents = this.events.slice(0, Math.min(this.MAX_EVENTS, this._h - this._eventsRow - 3));
|
|
705
|
+
for (let i = 0; i < visibleEvents.length; i++) {
|
|
706
|
+
const row = this._eventsRow + 1 + i;
|
|
707
|
+
if (row > this._h - 2) break;
|
|
708
|
+
const evt = visibleEvents[i];
|
|
709
|
+
const typeColor = evt.type === 'error' || evt.type === 'death' ? C.cmdError
|
|
710
|
+
: evt.type === 'warn' || evt.type === 'lowls' ? C.cmdWarn
|
|
711
|
+
: evt.type === 'success' || evt.type === 'levelup' ? C.cmdSuccess
|
|
712
|
+
: C.cmdEvent;
|
|
713
|
+
|
|
714
|
+
const line = ` ${dim}${evt.ts}${A.reset} ${typeColor}${evt.msg}${A.reset}`;
|
|
715
|
+
this._write(`${this._at(row, 1)}${b}${this._rpad(line, w - 2)}${r}`);
|
|
716
|
+
}
|
|
717
|
+
this._footerRow = this._eventsRow + 1 + visibleEvents.length;
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
_drawFooter() {
|
|
721
|
+
const w = this._w;
|
|
722
|
+
const b = C.border;
|
|
723
|
+
const dim = A.dim;
|
|
724
|
+
const r = A.reset;
|
|
725
|
+
const sep = `${b}${'─'.repeat(w - 2)}${r}`;
|
|
726
|
+
|
|
727
|
+
if (this._footerRow > this._h - 1) this._footerRow = this._h - 2;
|
|
728
|
+
this._write(`${this._at(this._footerRow, 1)}${sep}`);
|
|
729
|
+
|
|
730
|
+
const hint = `${dim}↑↓ scroll · j/k navigate · Ctrl+C quit${r}`;
|
|
731
|
+
this._write(`${this._at(this._footerRow + 1, 1)}${b} ${hint}${' '.repeat(Math.max(0, w - 2 - this._ansiLen(hint)))}${r}`);
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
_render() {
|
|
735
|
+
if (!READY || this._shutdown || !this._active) return;
|
|
736
|
+
|
|
737
|
+
// Auto-follow newest active worker
|
|
738
|
+
if (this._followWorkerIdx >= 0 && this._followWorkerIdx < this.workers.length) {
|
|
739
|
+
const target = this._followWorkerIdx;
|
|
740
|
+
if (target < this.windowStart) {
|
|
741
|
+
this.windowStart = target;
|
|
742
|
+
this.dirtyWorkers = new Set();
|
|
743
|
+
for (let i = this.windowStart; i < this.windowStart + this.windowSize; i++) {
|
|
744
|
+
if (i < this.workers.length) this.dirtyWorkers.add(i);
|
|
745
|
+
}
|
|
746
|
+
} else if (target >= this.windowStart + this.windowSize) {
|
|
747
|
+
this.windowStart = target - this.windowSize + 1;
|
|
748
|
+
this.dirtyWorkers = new Set();
|
|
749
|
+
for (let i = this.windowStart; i < this.windowStart + this.windowSize; i++) {
|
|
750
|
+
if (i < this.workers.length) this.dirtyWorkers.add(i);
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
// Redraw everything on resize changes
|
|
756
|
+
if (this.dirtyStats) {
|
|
757
|
+
this._drawHeader();
|
|
758
|
+
this.dirtyWorkers = new Set(this.workers.map((_, i) => i));
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
if (this.dirtyWorkers.size > 0) {
|
|
762
|
+
const w = this._w;
|
|
763
|
+
const b = C.border;
|
|
764
|
+
const r = A.reset;
|
|
765
|
+
const sep = `${b}${'─'.repeat(w - 2)}${r}`;
|
|
766
|
+
|
|
767
|
+
for (const idx of this.dirtyWorkers) {
|
|
768
|
+
const localIdx = idx - this.windowStart;
|
|
769
|
+
const row = this._accountsRow + 2 + localIdx;
|
|
770
|
+
if (row < this._accountsRow + 2 || row > this._h - 4) continue;
|
|
771
|
+
|
|
772
|
+
if (idx < this.workers.length) {
|
|
773
|
+
const line = this._buildAccountRow(this.workers[idx], idx);
|
|
774
|
+
this._write(`${this._at(row, 1)}${b}${line}${r}`);
|
|
775
|
+
} else {
|
|
776
|
+
this._write(`${this._at(row, 1)}${b}${' '.repeat(w - 2)}${r}`);
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
if (this.dirtyEvents) {
|
|
782
|
+
this._drawEvents();
|
|
783
|
+
this._drawFooter();
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
this.dirtyWorkers.clear();
|
|
787
|
+
this.dirtyEvents = false;
|
|
788
|
+
this.dirtyStats = false;
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
_startRenderLoop() {
|
|
792
|
+
if (this._renderTimer) clearInterval(this._renderTimer);
|
|
793
|
+
this._renderTimer = setInterval(() => this._render(), 250); // 4 FPS
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
_printSummaryPlain(summary) {
|
|
797
|
+
const { totalCoins = 0, totalCmds = 0, totalSuccess = 0,
|
|
798
|
+
workers = [], uptime = 0, memMB = 0 } = summary;
|
|
799
|
+
const b = '═'.repeat(60);
|
|
800
|
+
console.log('');
|
|
801
|
+
console.log(` ${'═'.repeat(60)}`);
|
|
802
|
+
console.log(` ⬡ DANKGRINDER — Session Summary`);
|
|
803
|
+
console.log(` ${'─'.repeat(60)}`);
|
|
804
|
+
for (const wk of workers) {
|
|
805
|
+
const rate = wk.stats?.commands > 0
|
|
806
|
+
? `${((wk.stats.successes / wk.stats.commands) * 100).toFixed(0)}%`
|
|
807
|
+
: '0%';
|
|
808
|
+
console.log(
|
|
809
|
+
` 💎 ${(wk.username || '?').padEnd(18)} +⏣ ${(wk.stats?.coins || 0).toLocaleString().padStart(8)} ` +
|
|
810
|
+
`${(wk.stats?.commands || 0)}cmds ${rate} ♥${wk._lifesavers ?? '?'} Lv.${wk._level ?? '?'}`
|
|
811
|
+
);
|
|
812
|
+
}
|
|
813
|
+
console.log(` ${'─'.repeat(60)}`);
|
|
814
|
+
console.log(` 💰 Total: ⏣ ${totalCoins.toLocaleString()} ${totalCmds}cmds ${totalSuccess}%OK ${this._fmtUptime(uptime)} ${memMB}MB`);
|
|
815
|
+
console.log(` ${'═'.repeat(60)}`);
|
|
816
|
+
console.log('');
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
module.exports = new Terminal();
|