dankgrinder 7.83.0 → 8.2.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 +71 -81
- package/package.json +1 -1
- package/lib/terminal.js +0 -720
package/lib/grinder.js
CHANGED
|
@@ -3,7 +3,6 @@ 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');
|
|
7
6
|
const {
|
|
8
7
|
BloomFilter, RingBuffer, TokenBucket, EMA, SlidingWindowCounter,
|
|
9
8
|
AhoCorasick, LRUCache, StringPool, AsyncBatchQueue, JitterBackoff,
|
|
@@ -130,8 +129,6 @@ let API_URL = '';
|
|
|
130
129
|
let REDIS_URL = process.env.REDIS_URL || '';
|
|
131
130
|
let redis = null;
|
|
132
131
|
let workers = [];
|
|
133
|
-
let startTime = 0;
|
|
134
|
-
let shutdownCalled = false;
|
|
135
132
|
|
|
136
133
|
// ── Cluster Mode Config ──────────────────────────────────────
|
|
137
134
|
// NODE_ID uniquely identifies this process in a multi-node cluster.
|
|
@@ -339,17 +336,6 @@ function colorBanner() {
|
|
|
339
336
|
|
|
340
337
|
// ── Simple Logging ─────────────────────────────────────────────
|
|
341
338
|
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
|
-
|
|
353
339
|
const colorIcons = {
|
|
354
340
|
info: `${c.dim}·${c.reset}`, success: `${rgb(52, 211, 153)}✓${c.reset}`,
|
|
355
341
|
error: `${rgb(239, 68, 68)}✗${c.reset}`, warn: `${rgb(251, 191, 36)}!${c.reset}`,
|
|
@@ -1635,8 +1621,6 @@ class AccountWorker {
|
|
|
1635
1621
|
// ── Death / lifesaver detection in command responses ──
|
|
1636
1622
|
if (resultLower.includes('you died') || resultLower.includes('lifesaver protected')) {
|
|
1637
1623
|
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);
|
|
1640
1624
|
// Check for lifesaver count in the response
|
|
1641
1625
|
const lsMatch = result.match(/(\d+)\s*life\s*saver/i);
|
|
1642
1626
|
const lsCount = lsMatch ? parseInt(lsMatch[1]) : -1;
|
|
@@ -1653,8 +1637,6 @@ class AccountWorker {
|
|
|
1653
1637
|
await redis.set(`dkg:lifesavers:${this.account.id}`, String(lsCount), 'EX', 86400);
|
|
1654
1638
|
this.log('warn', `Lifesaver used! ${lsCount} remaining.`);
|
|
1655
1639
|
if (lsCount <= 2) {
|
|
1656
|
-
terminal.flashEvent('lowls', `⚠ ${this.username} low lifesavers: ${lsCount} left`);
|
|
1657
|
-
terminal.markWorkerDirty(this.idx);
|
|
1658
1640
|
sendWebhook('LOW LIFESAVERS', `**${this.username}** has only **${lsCount}** lifesaver(s) left!`, 0xfbbf24);
|
|
1659
1641
|
}
|
|
1660
1642
|
}
|
|
@@ -1774,8 +1756,6 @@ class AccountWorker {
|
|
|
1774
1756
|
this.setStatus(formattedResult);
|
|
1775
1757
|
await sendLog(this.username, cmdName, result, 'success');
|
|
1776
1758
|
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()}`);
|
|
1779
1759
|
|
|
1780
1760
|
// Auto-sell fish every 5 fishing rounds
|
|
1781
1761
|
if (cmdName === 'fish') {
|
|
@@ -2759,8 +2739,12 @@ async function start(apiKey, apiUrl, opts = {}) {
|
|
|
2759
2739
|
API_KEY = apiKey;
|
|
2760
2740
|
API_URL = apiUrl || process.env.DANKGRINDER_URL || 'http://localhost:3000';
|
|
2761
2741
|
const CLOUD_MODE = opts.cloud === true;
|
|
2762
|
-
startTime = Date.now();
|
|
2763
2742
|
|
|
2743
|
+
if (CLOUD_MODE) {
|
|
2744
|
+
// In cloud mode, API_KEY is the CLOUD_ADMIN_KEY — not used for user auth.
|
|
2745
|
+
// Per-account keys are fetched per-account from /api/cloud/grinders.
|
|
2746
|
+
console.log('🌥️ Starting in CLOUD MODE — grinding all cloud-enabled accounts');
|
|
2747
|
+
}
|
|
2764
2748
|
REDIS_URL = process.env.REDIS_URL || '';
|
|
2765
2749
|
WEBHOOK_URL = process.env.WEBHOOK_URL || '';
|
|
2766
2750
|
|
|
@@ -2768,14 +2752,15 @@ async function start(apiKey, apiUrl, opts = {}) {
|
|
|
2768
2752
|
let hasZlib = false;
|
|
2769
2753
|
try { require('zlib-sync'); hasZlib = true; } catch {}
|
|
2770
2754
|
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
|
|
2777
|
-
}
|
|
2778
|
-
|
|
2755
|
+
console.log(colorBanner());
|
|
2756
|
+
console.log(
|
|
2757
|
+
` ${rgb(139, 92, 246)}v${PKG_VERSION}${c.reset}` +
|
|
2758
|
+
` ${c.dim}·${c.reset} ${c.white}${AccountWorker.COMMAND_MAP.length} Commands${c.reset}` +
|
|
2759
|
+
` ${c.dim}·${c.reset} ${rgb(34, 211, 238)}${CLOUD_MODE ? 'Cloud Mode' : (CLUSTER_ENABLED ? 'Cluster Mode' : 'Standalone')}${c.reset}` +
|
|
2760
|
+
` ${c.dim}·${c.reset} ${rgb(52, 211, 153)}Auto-Recovery${c.reset}` +
|
|
2761
|
+
` ${c.dim}·${c.reset} ${rgb(251, 191, 36)}Loss Limiter${c.reset}`
|
|
2762
|
+
);
|
|
2763
|
+
log('info', `${c.dim}Fetching accounts...${c.reset}`);
|
|
2779
2764
|
|
|
2780
2765
|
const fetchOpts = CLOUD_MODE ? { cloud: true } : {};
|
|
2781
2766
|
let data = await fetchConfig(4, 2000, fetchOpts);
|
|
@@ -2786,11 +2771,9 @@ async function start(apiKey, apiUrl, opts = {}) {
|
|
|
2786
2771
|
data = await fetchConfig(4, 2000, fetchOpts);
|
|
2787
2772
|
}
|
|
2788
2773
|
if (data && data.error) {
|
|
2789
|
-
terminal.endPhase(`API error: ${data.error}`, false);
|
|
2790
2774
|
log('error', `${data.error}`);
|
|
2791
2775
|
return;
|
|
2792
2776
|
}
|
|
2793
|
-
terminal.endPhase(`API connected — ${data.accounts?.length || 0} accounts`);
|
|
2794
2777
|
|
|
2795
2778
|
// Cloud mode: post heartbeat every 30s
|
|
2796
2779
|
if (CLOUD_MODE) {
|
|
@@ -2835,6 +2818,10 @@ async function start(apiKey, apiUrl, opts = {}) {
|
|
|
2835
2818
|
}
|
|
2836
2819
|
}
|
|
2837
2820
|
|
|
2821
|
+
const checks = [];
|
|
2822
|
+
checks.push(`${rgb(52, 211, 153)}✓${c.reset} ${c.white}API${c.reset}`);
|
|
2823
|
+
if (REDIS_URL) checks.push(redis ? `${rgb(52, 211, 153)}✓${c.reset} ${c.white}Redis${c.reset}` : `${rgb(251, 191, 36)}○${c.reset} ${c.dim}Redis (connecting...)${c.reset}`);
|
|
2824
|
+
|
|
2838
2825
|
// Init rawLogger Redis (uses same URL — logs all raw gateway data)
|
|
2839
2826
|
if (REDIS_URL) {
|
|
2840
2827
|
rawLogger.init(redis);
|
|
@@ -2854,8 +2841,6 @@ async function start(apiKey, apiUrl, opts = {}) {
|
|
|
2854
2841
|
w.log?.('error', `DEATH in DMs! 0 lifesavers — disabling crime/search`);
|
|
2855
2842
|
w.setCooldown?.('crime', 86400);
|
|
2856
2843
|
w.setCooldown?.('search', 86400);
|
|
2857
|
-
terminal.flashEvent('death', `💀 ${w.username} DEATH in DM — 0 lifesavers!`);
|
|
2858
|
-
terminal.markWorkerDirty(w.idx);
|
|
2859
2844
|
sendWebhook?.('DEATH ALERT (DM)', `**${w.username}** died in DMs! **0 lifesavers!**\nCrime/search auto-disabled.`, 0xef4444);
|
|
2860
2845
|
} else {
|
|
2861
2846
|
w.log?.('warn', `DEATH in DMs — ${event.lifesaversLeft} lifesavers remaining`);
|
|
@@ -2873,20 +2858,23 @@ async function start(apiKey, apiUrl, opts = {}) {
|
|
|
2873
2858
|
if (event.type === 'levelup') {
|
|
2874
2859
|
if (event.to > 0) {
|
|
2875
2860
|
w._level = event.to;
|
|
2876
|
-
terminal.flashEvent('levelup', `⬆️ ${w.username} leveled up to Lv.${event.to}`);
|
|
2877
|
-
terminal.markWorkerDirty(w.idx);
|
|
2878
2861
|
}
|
|
2879
2862
|
}
|
|
2880
2863
|
}
|
|
2881
2864
|
});
|
|
2865
|
+
checks.push(`${rgb(52, 211, 153)}✓${c.reset} ${c.white}RawLog${c.reset}`);
|
|
2882
2866
|
}
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
|
|
2886
|
-
|
|
2887
|
-
|
|
2867
|
+
if (hasZlib) checks.push(`${rgb(52, 211, 153)}✓${c.reset} ${c.white}zlib${c.reset}`);
|
|
2868
|
+
if (WEBHOOK_URL) checks.push(`${rgb(52, 211, 153)}✓${c.reset} ${c.white}Webhook${c.reset}`);
|
|
2869
|
+
if (CLUSTER_ENABLED) {
|
|
2870
|
+
checks.push(`${rgb(52, 211, 153)}✓${c.reset} ${rgb(34, 211, 238)}Cluster${c.reset} ${c.dim}(${NODE_ID.substring(0, 12)})${c.reset}`);
|
|
2871
|
+
}
|
|
2872
|
+
checks.push(`${rgb(52, 211, 153)}✓${c.reset} ${c.white}${accounts.length} Account${accounts.length > 1 ? 's' : ''}${c.reset}`);
|
|
2873
|
+
console.log(` ${checks.join(' ')}`);
|
|
2874
|
+
console.log('');
|
|
2888
2875
|
|
|
2889
2876
|
// ── Phase 1: Login accounts ─────────────────────────────────────────
|
|
2877
|
+
console.log(` ${rgb(52, 211, 153)}✓${c.reset} Logging in ${accounts.length} account(s)...`);
|
|
2890
2878
|
const parsedGapMin = Number.parseInt(String(process.env.LOGIN_GAP_MIN_MS || '50'), 10);
|
|
2891
2879
|
const parsedGapMax = Number.parseInt(String(process.env.LOGIN_GAP_MAX_MS || '150'), 10);
|
|
2892
2880
|
const LOGIN_GAP_MIN_MS = Number.isFinite(parsedGapMin) && parsedGapMin >= 0 ? parsedGapMin : 50;
|
|
@@ -2903,7 +2891,6 @@ async function start(apiKey, apiUrl, opts = {}) {
|
|
|
2903
2891
|
workers.push(worker);
|
|
2904
2892
|
workerMap.set(acc.id, worker);
|
|
2905
2893
|
await worker.start();
|
|
2906
|
-
terminal.updateProgress(workers.length, accounts.length);
|
|
2907
2894
|
}));
|
|
2908
2895
|
if (i + BATCH_SIZE < accounts.length) await new Promise(r => setTimeout(r, randomLoginGap()));
|
|
2909
2896
|
hintGC();
|
|
@@ -2912,9 +2899,8 @@ async function start(apiKey, apiUrl, opts = {}) {
|
|
|
2912
2899
|
const loginDone = workers.filter(w => !w._tokenInvalid && w.channel).length;
|
|
2913
2900
|
const invalidWorkers = workers.filter(w => w._tokenInvalid);
|
|
2914
2901
|
const timedOutWorkers = workers.filter(w => !w.channel && !w._tokenInvalid);
|
|
2915
|
-
|
|
2902
|
+
console.log(` ${rgb(52, 211, 153)}✓${c.reset} ${loginDone}/${accounts.length} accounts connected`);
|
|
2916
2903
|
if (invalidWorkers.length > 0) {
|
|
2917
|
-
terminal.flashEvent('error', `${invalidWorkers.length} accounts have INVALID tokens`);
|
|
2918
2904
|
log('warn', `${rgb(239, 68, 68)}${invalidWorkers.length} account(s) have INVALID tokens`);
|
|
2919
2905
|
for (const w of invalidWorkers) log('error', ` ${w.account.label || w.account.id} — token invalid or expired`);
|
|
2920
2906
|
}
|
|
@@ -2923,31 +2909,24 @@ async function start(apiKey, apiUrl, opts = {}) {
|
|
|
2923
2909
|
const activeWorkers = workers.filter(w => !w._tokenInvalid);
|
|
2924
2910
|
|
|
2925
2911
|
// ── Phase 2: Inventory check ─────────────────────────────────────
|
|
2926
|
-
|
|
2927
|
-
terminal.updateProgress(0, activeWorkers.length);
|
|
2912
|
+
console.log(` ${rgb(34, 211, 238)}·${c.reset} Checking inventory (${activeWorkers.length} accounts)...`);
|
|
2928
2913
|
let invDone = 0, invFailed = 0;
|
|
2929
2914
|
await Promise.all(activeWorkers.map(async (w) => {
|
|
2930
2915
|
try { await w.checkInventory({ force: true, requireComplete: true, maxAttempts: 3, silent: true }); }
|
|
2931
2916
|
catch { invFailed++; return; }
|
|
2932
2917
|
invDone++;
|
|
2933
|
-
terminal.updateProgress(invDone, activeWorkers.length);
|
|
2934
2918
|
}));
|
|
2935
2919
|
|
|
2936
2920
|
if (invFailed > 0) {
|
|
2937
|
-
terminal.endPhase(`Inventory: ${invFailed} failed`, false);
|
|
2938
2921
|
console.log(` ${rgb(239, 68, 68)}✗${c.reset} Inventory: ${invFailed} failed — not starting grind loops`);
|
|
2939
2922
|
return;
|
|
2940
2923
|
}
|
|
2941
|
-
|
|
2924
|
+
console.log(` ${rgb(52, 211, 153)}✓${c.reset} Inventory: ${invDone}/${activeWorkers.length} clear`);
|
|
2942
2925
|
|
|
2943
2926
|
// ── Phase 2.5: Balance check ────────────────────────────────────
|
|
2944
|
-
|
|
2945
|
-
terminal.updateProgress(0, activeWorkers.length);
|
|
2946
|
-
let balDone = 0;
|
|
2927
|
+
console.log(` ${rgb(251, 191, 36)}·${c.reset} Checking balance (${activeWorkers.length} accounts)...`);
|
|
2947
2928
|
await Promise.all(activeWorkers.map(async (w) => {
|
|
2948
2929
|
try { await w.checkBalance(true); } catch {}
|
|
2949
|
-
balDone++;
|
|
2950
|
-
terminal.updateProgress(balDone, activeWorkers.length);
|
|
2951
2930
|
}));
|
|
2952
2931
|
|
|
2953
2932
|
let totalWallet = 0, totalBank = 0, noLifesaverAccounts = [];
|
|
@@ -2956,16 +2935,13 @@ async function start(apiKey, apiUrl, opts = {}) {
|
|
|
2956
2935
|
totalBank += w.stats?.bankBalance || 0;
|
|
2957
2936
|
if (w._lifesavers === 0) noLifesaverAccounts.push(w.username || w.account.label);
|
|
2958
2937
|
}
|
|
2959
|
-
|
|
2938
|
+
console.log(` ${rgb(52, 211, 153)}✓${c.reset} Balance: ${c.green}⏣ ${(totalWallet + totalBank).toLocaleString()}${c.reset} ${c.dim}(wallet: ⏣ ${totalWallet.toLocaleString()} + bank: ⏣ ${totalBank.toLocaleString()})${c.reset}`);
|
|
2960
2939
|
if (noLifesaverAccounts.length > 0) {
|
|
2961
|
-
terminal.flashEvent('warn', `${noLifesaverAccounts.length} accounts have 0 LIFESAVERS`);
|
|
2962
2940
|
console.log(` ${rgb(239, 68, 68)}⚠${c.reset} ${noLifesaverAccounts.length} account(s) have 0 LIFESAVERS — crime/search disabled`);
|
|
2963
2941
|
}
|
|
2964
2942
|
|
|
2965
2943
|
// ── Phase 2.75: DM history check ────────────────────────────────
|
|
2966
|
-
|
|
2967
|
-
terminal.updateProgress(0, activeWorkers.length);
|
|
2968
|
-
let dmDone = 0;
|
|
2944
|
+
console.log(` ${rgb(139, 92, 246)}·${c.reset} Checking DM history...`);
|
|
2969
2945
|
let dmDeaths = 0, dmLevelUps = 0, dmNoLs = [], dmUnknown = [];
|
|
2970
2946
|
for (const w of activeWorkers) {
|
|
2971
2947
|
try {
|
|
@@ -2977,11 +2953,8 @@ async function start(apiKey, apiUrl, opts = {}) {
|
|
|
2977
2953
|
if (dm.currentLevel > 0) w._level = dm.currentLevel;
|
|
2978
2954
|
if (dm.lifesavers >= 0) w._lifesavers = dm.lifesavers;
|
|
2979
2955
|
} catch {}
|
|
2980
|
-
dmDone++;
|
|
2981
|
-
terminal.updateProgress(dmDone, activeWorkers.length);
|
|
2982
2956
|
}
|
|
2983
2957
|
if (dmNoLs.length > 0) {
|
|
2984
|
-
terminal.flashEvent('warn', `No lifesavers: ${dmNoLs.join(', ')}`);
|
|
2985
2958
|
log('warn', `⚠ No lifesavers: ${dmNoLs.join(', ')}`);
|
|
2986
2959
|
for (const w of activeWorkers) {
|
|
2987
2960
|
if (dmNoLs.includes(w.username) && redis) {
|
|
@@ -2999,16 +2972,17 @@ async function start(apiKey, apiUrl, opts = {}) {
|
|
|
2999
2972
|
if (dmDeaths > 0) dmSummaryParts.push(`${dmDeaths} deaths`);
|
|
3000
2973
|
if (dmLevelUps > 0) dmSummaryParts.push(`${dmLevelUps} level-ups`);
|
|
3001
2974
|
if (dmUnknown.length > 0) dmSummaryParts.push(`${dmUnknown.length} pending`);
|
|
3002
|
-
|
|
2975
|
+
console.log(` ${rgb(52, 211, 153)}✓${c.reset} DM check: ${dmSummaryParts.length > 0 ? dmSummaryParts.join(', ') : 'clean'}`);
|
|
3003
2976
|
|
|
3004
2977
|
// ── Phase 3: Start grind loops ───────────────────────────────────
|
|
3005
|
-
|
|
2978
|
+
console.log(` ${rgb(139, 92, 246)}>>>${c.reset} Starting grind loops...`);
|
|
2979
|
+
// Phase 3: Start all grind loops (only for valid workers)
|
|
3006
2980
|
for (const w of activeWorkers) {
|
|
3007
2981
|
if (!shutdownCalled) w.grindLoop();
|
|
3008
2982
|
}
|
|
3009
|
-
|
|
3010
|
-
|
|
3011
|
-
|
|
2983
|
+
|
|
2984
|
+
console.log(` ${rgb(52, 211, 153)}✓${c.reset} All grind loops started — ${activeWorkers.length} accounts active`);
|
|
2985
|
+
console.log(` v${PKG_VERSION} | press Ctrl+C to stop`);
|
|
3012
2986
|
|
|
3013
2987
|
// Cluster heartbeat — lets other nodes see this node is alive
|
|
3014
2988
|
if (CLUSTER_ENABLED) {
|
|
@@ -3075,26 +3049,34 @@ async function start(apiKey, apiUrl, opts = {}) {
|
|
|
3075
3049
|
sigintHandled = true;
|
|
3076
3050
|
shutdownCalled = true;
|
|
3077
3051
|
setDashboardActive(false);
|
|
3052
|
+
process.stdout.write(c.show);
|
|
3078
3053
|
|
|
3079
|
-
|
|
3080
|
-
|
|
3054
|
+
const sepBar = rgb(139, 92, 246) + c.bold + '═'.repeat(tw) + c.reset;
|
|
3055
|
+
console.log('');
|
|
3056
|
+
console.log(` ${rgb(251, 191, 36)}${c.bold}Session Summary${c.reset}`);
|
|
3057
|
+
console.log(sepBar);
|
|
3058
|
+
|
|
3059
|
+
// Collect stats from all workers (including rotated-out ones)
|
|
3060
|
+
let finalCoins = 0;
|
|
3061
|
+
let finalCmds = 0;
|
|
3081
3062
|
for (const wk of workers) {
|
|
3063
|
+
const rate = wk.stats.commands > 0 ? ((wk.stats.successes / wk.stats.commands) * 100).toFixed(0) : 0;
|
|
3064
|
+
console.log(
|
|
3065
|
+
` ${wk.color}${c.bold}${(wk.username || '?').padEnd(18)}${c.reset}` +
|
|
3066
|
+
` ${rgb(52, 211, 153)}+⏣ ${wk.stats.coins.toLocaleString().padStart(8)}${c.reset}` +
|
|
3067
|
+
` ${c.dim}${wk.stats.commands.toString().padStart(4)} cmds${c.reset}` +
|
|
3068
|
+
` ${c.dim}${rate}% success${c.reset}`
|
|
3069
|
+
);
|
|
3082
3070
|
finalCoins += wk.stats.coins || 0;
|
|
3083
3071
|
finalCmds += wk.stats.commands || 0;
|
|
3084
|
-
totalSuccess += wk.stats.successes || 0;
|
|
3085
3072
|
}
|
|
3073
|
+
console.log(sepBar);
|
|
3074
|
+
|
|
3086
3075
|
const memFinal = Math.round((process.memoryUsage?.rss?.() ?? process.memoryUsage().rss) / 1048576);
|
|
3087
|
-
const
|
|
3088
|
-
|
|
3089
|
-
|
|
3090
|
-
|
|
3091
|
-
totalCoins: finalCoins,
|
|
3092
|
-
totalCmds: finalCmds,
|
|
3093
|
-
totalSuccess: finalCmds > 0 ? ((totalSuccess / finalCmds) * 100).toFixed(0) : 0,
|
|
3094
|
-
workers,
|
|
3095
|
-
uptime: uptimeMs,
|
|
3096
|
-
memMB: memFinal,
|
|
3097
|
-
});
|
|
3076
|
+
const avgEarn = globalEarningsEMA.get();
|
|
3077
|
+
const cpm = globalCmdRate.getRate().toFixed(1);
|
|
3078
|
+
console.log(` ${rgb(251, 191, 36)}${c.bold}Total: +⏣ ${finalCoins.toLocaleString()}${c.reset} ${c.dim}in ${formatUptime()} | ${finalCmds} cmds | ~${cpm} cmd/m | ${memFinal}MB | avg earn ⏣ ${Math.round(avgEarn)}${c.reset}`);
|
|
3079
|
+
console.log('');
|
|
3098
3080
|
|
|
3099
3081
|
// Release all cluster claims before stopping workers
|
|
3100
3082
|
const releasePromises = workers.map(wk => releaseClaim(wk.account.id).catch(() => {}));
|
|
@@ -3110,14 +3092,22 @@ async function start(apiKey, apiUrl, opts = {}) {
|
|
|
3110
3092
|
|
|
3111
3093
|
const totalRecoveries = workers.reduce((sum, wk) => sum + (wk._totalRecoveries || 0), 0);
|
|
3112
3094
|
const totalDisconnects = workers.reduce((sum, wk) => sum + (wk._disconnectCount || 0), 0);
|
|
3095
|
+
const totalRateLimits = workers.reduce((sum, wk) => sum + (wk._rateLimitHits || 0), 0);
|
|
3113
3096
|
|
|
3114
3097
|
const webhookMsg = `+⏣ ${finalCoins.toLocaleString()} | ${finalCmds} cmds | ${formatUptime()}` +
|
|
3115
3098
|
(totalRecoveries > 0 ? ` | ${totalRecoveries} auto-recoveries` : '') +
|
|
3116
3099
|
(CLUSTER_ENABLED ? ` | node: ${NODE_ID.substring(0, 12)}` : '');
|
|
3117
3100
|
sendWebhook('Session Ended', webhookMsg, 0x8b5cf6);
|
|
3118
3101
|
|
|
3102
|
+
if (totalRecoveries > 0 || totalDisconnects > 0) {
|
|
3103
|
+
console.log(` ${c.dim}Recovery stats: ${totalRecoveries} auto-recoveries, ${totalDisconnects} disconnects, ${totalRateLimits} rate limits${c.reset}`);
|
|
3104
|
+
}
|
|
3105
|
+
if (CLUSTER_ENABLED) {
|
|
3106
|
+
console.log(` ${c.dim}Cluster node: ${NODE_ID} — claims released${c.reset}`);
|
|
3107
|
+
}
|
|
3108
|
+
|
|
3119
3109
|
if (redis) { try { redis.disconnect(); } catch {} }
|
|
3120
|
-
setTimeout(() => process.exit(0),
|
|
3110
|
+
setTimeout(() => process.exit(0), 1500);
|
|
3121
3111
|
});
|
|
3122
3112
|
}
|
|
3123
3113
|
|
package/package.json
CHANGED
package/lib/terminal.js
DELETED
|
@@ -1,720 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* terminal.js — Polished animated terminal renderer for DankGrinder
|
|
3
|
-
*
|
|
4
|
-
* Features:
|
|
5
|
-
* - Animated startup phases with multi-element spinners
|
|
6
|
-
* - Live leaderboard with coin-based rank medals (🥇🥈🥉)
|
|
7
|
-
* - Accounts stay in fixed positions, medals update live
|
|
8
|
-
* - Pulsing status indicators for active accounts
|
|
9
|
-
* - Dark theme: deep purple borders, vibrant accent colors
|
|
10
|
-
* - 4 FPS render loop, dirty-row only, no flicker
|
|
11
|
-
* - Graceful fallback: plain console.log if not a TTY
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
'use strict';
|
|
15
|
-
|
|
16
|
-
const READY = (() => {
|
|
17
|
-
try { return !!process.stdout.isTTY && !process.env.NO_TERM; } catch (_) { return false; }
|
|
18
|
-
})();
|
|
19
|
-
|
|
20
|
-
// ── ANSI helpers ────────────────────────────────────────────────────────────
|
|
21
|
-
|
|
22
|
-
const A = {
|
|
23
|
-
reset: '\x1b[0m',
|
|
24
|
-
bold: '\x1b[1m',
|
|
25
|
-
dim: '\x1b[2m',
|
|
26
|
-
rgb: (r, g, b) => `\x1b[38;2;${r};${g};${b}m`,
|
|
27
|
-
eraseAll: '\x1b[3J\x1b[2J\x1b[H',
|
|
28
|
-
clearLine: '\x1b[2K',
|
|
29
|
-
save: '\x1b7',
|
|
30
|
-
restore: '\x1b8',
|
|
31
|
-
hide: '\x1b[?25l',
|
|
32
|
-
show: '\x1b[?25h',
|
|
33
|
-
};
|
|
34
|
-
|
|
35
|
-
// ── Palette ─────────────────────────────────────────────────────────────────
|
|
36
|
-
|
|
37
|
-
const C = {
|
|
38
|
-
border: A.rgb(55, 42, 95),
|
|
39
|
-
borderDim: A.rgb(38, 28, 65),
|
|
40
|
-
borderMid: A.rgb(80, 65, 130),
|
|
41
|
-
|
|
42
|
-
text: A.rgb(205, 200, 230),
|
|
43
|
-
textDim: A.rgb(90, 85, 115),
|
|
44
|
-
textFaint: A.rgb(55, 50, 75),
|
|
45
|
-
|
|
46
|
-
purple: A.rgb(167, 139, 250), // vivid lavender
|
|
47
|
-
cyan: A.rgb(34, 211, 238), // vivid cyan
|
|
48
|
-
gold: A.rgb(251, 191, 36), // vivid gold
|
|
49
|
-
green: A.rgb(52, 211, 153), // vivid green
|
|
50
|
-
pink: A.rgb(244, 114, 182), // vivid pink
|
|
51
|
-
orange: A.rgb(251, 146, 60), // vivid orange
|
|
52
|
-
red: A.rgb(248, 113, 113), // vivid red
|
|
53
|
-
blue: A.rgb(96, 165, 250), // vivid blue
|
|
54
|
-
|
|
55
|
-
// Rank medal colors
|
|
56
|
-
rank1: A.rgb(255, 215, 0),
|
|
57
|
-
rank2: A.rgb(192, 192, 192),
|
|
58
|
-
rank3: A.rgb(205, 127, 50),
|
|
59
|
-
|
|
60
|
-
// Per-account accent colors (cycles)
|
|
61
|
-
ACCT: [
|
|
62
|
-
A.rgb(167, 139, 250), // lavender
|
|
63
|
-
A.rgb(103, 232, 249), // sky cyan
|
|
64
|
-
A.rgb(253, 186, 116), // peach
|
|
65
|
-
A.rgb(167, 243, 208), // mint
|
|
66
|
-
A.rgb(252, 165, 201), // rose
|
|
67
|
-
A.rgb(165, 243, 252), // light cyan
|
|
68
|
-
A.rgb(196, 181, 253), // light purple
|
|
69
|
-
A.rgb(147, 226, 226), // light teal
|
|
70
|
-
],
|
|
71
|
-
};
|
|
72
|
-
|
|
73
|
-
// ── Box-drawing ─────────────────────────────────────────────────────────────
|
|
74
|
-
|
|
75
|
-
const TL='╭', TR='╮', BL='╰', BR='╯', H='─', V='│';
|
|
76
|
-
|
|
77
|
-
// ── Spinner frames ──────────────────────────────────────────────────────────
|
|
78
|
-
|
|
79
|
-
// Dot spinner for phase labels
|
|
80
|
-
const SPIN_DOTS = ['⠋','⠙','⠹','⠸','⠼','⠴','⠦','⠧','⠿'];
|
|
81
|
-
|
|
82
|
-
// Block spinner for progress
|
|
83
|
-
const SPIN_BLOCK = ['▏','▎','�','▌','▋','▊','▉','▊'];
|
|
84
|
-
|
|
85
|
-
// Pulse frames for active indicators
|
|
86
|
-
const PULSE = ['●', '◉', '◎', '○'];
|
|
87
|
-
|
|
88
|
-
// ── stdout capture ──────────────────────────────────────────────────────────
|
|
89
|
-
|
|
90
|
-
let _origWrite = null;
|
|
91
|
-
let _captureActive = false;
|
|
92
|
-
let _captureBuf = [];
|
|
93
|
-
|
|
94
|
-
function _capWrite(chunk) {
|
|
95
|
-
if (_captureActive) { _captureBuf.push(String(chunk)); return; }
|
|
96
|
-
return _origWrite.call(process.stdout, chunk);
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
// ── ANSI utils ─────────────────────────────────────────────────────────────
|
|
100
|
-
|
|
101
|
-
function ansiLen(s) {
|
|
102
|
-
let len = 0, i = 0;
|
|
103
|
-
const str = String(s);
|
|
104
|
-
while (i < str.length) {
|
|
105
|
-
if (str.charCodeAt(i) === 0x1b && str[i+1] === '[') {
|
|
106
|
-
let j = i+2;
|
|
107
|
-
while (j < str.length && str[j] !== 'm') j++;
|
|
108
|
-
i = j + 1;
|
|
109
|
-
} else { len++; i++; }
|
|
110
|
-
}
|
|
111
|
-
return len;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
function rpad(s, w) { return s + ' '.repeat(Math.max(0, w - ansiLen(s))); }
|
|
115
|
-
|
|
116
|
-
// ── Rank helpers ───────────────────────────────────────────────────────────
|
|
117
|
-
|
|
118
|
-
const MEDALS = ['🥇','🥈','🥉'];
|
|
119
|
-
|
|
120
|
-
function coinRank(wk, workers) {
|
|
121
|
-
const c = wk.stats?.coins || 0;
|
|
122
|
-
let r = 1;
|
|
123
|
-
for (const w of workers) {
|
|
124
|
-
if ((w.stats?.coins || 0) > c) r++;
|
|
125
|
-
}
|
|
126
|
-
return r;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
// ── Terminal ───────────────────────────────────────────────────────────────
|
|
130
|
-
|
|
131
|
-
class Terminal {
|
|
132
|
-
constructor() {
|
|
133
|
-
this.workers = [];
|
|
134
|
-
this.events = [];
|
|
135
|
-
this.MAX_EVENTS = 3;
|
|
136
|
-
|
|
137
|
-
this.phase = '';
|
|
138
|
-
this.phaseFrame = 0;
|
|
139
|
-
this.phaseTimer = null;
|
|
140
|
-
this.phaseDone = 0;
|
|
141
|
-
this.phaseTotal = 0;
|
|
142
|
-
|
|
143
|
-
this.dirtyWorkers = new Set();
|
|
144
|
-
this.dirtyStats = true;
|
|
145
|
-
this._renderTimer = null;
|
|
146
|
-
this._startTime = 0;
|
|
147
|
-
this._active = false;
|
|
148
|
-
this._shutdown = false;
|
|
149
|
-
this._origLog = null;
|
|
150
|
-
|
|
151
|
-
this._w = 110;
|
|
152
|
-
this._h = 35;
|
|
153
|
-
this.windowStart = 0;
|
|
154
|
-
this.windowSize = 8;
|
|
155
|
-
|
|
156
|
-
// Pulse animation state
|
|
157
|
-
this._pulseFrame = 0;
|
|
158
|
-
this._pulseTimer = null;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
// ── Public API ──────────────────────────────────────────────────────────
|
|
162
|
-
|
|
163
|
-
init(opts = {}) {
|
|
164
|
-
this._startTime = opts.startTime || Date.now();
|
|
165
|
-
this.workers = opts.workers || [];
|
|
166
|
-
this._updateSize();
|
|
167
|
-
|
|
168
|
-
if (READY) {
|
|
169
|
-
_origWrite = process.stdout.write.bind(process.stdout);
|
|
170
|
-
process.stdout.write = _capWrite;
|
|
171
|
-
_captureActive = true;
|
|
172
|
-
_captureBuf = [];
|
|
173
|
-
this._origLog = console.log;
|
|
174
|
-
console.log = (...args) => {
|
|
175
|
-
const s = args.join(' ');
|
|
176
|
-
if (this._active) {
|
|
177
|
-
const clean = s.replace(/\x1b\[[0-9;]*m/g, '').substring(0, 100);
|
|
178
|
-
if (clean.trim()) this.flashEvent('info', clean);
|
|
179
|
-
} else {
|
|
180
|
-
_capWrite(s + '\n');
|
|
181
|
-
}
|
|
182
|
-
};
|
|
183
|
-
process.stdout.on('resize', () => this._onResize());
|
|
184
|
-
this._drawStartupScreen();
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
setVersion(v) { this._version = v; }
|
|
189
|
-
|
|
190
|
-
startPhase(name) {
|
|
191
|
-
this.phase = name;
|
|
192
|
-
this.phaseDone = 0;
|
|
193
|
-
this.phaseTotal = 0;
|
|
194
|
-
this.phaseFrame = 0;
|
|
195
|
-
if (!READY) {
|
|
196
|
-
process.stdout.write(`\n ⏳ ${name}\n`);
|
|
197
|
-
return;
|
|
198
|
-
}
|
|
199
|
-
if (this.phaseTimer) clearInterval(this.phaseTimer);
|
|
200
|
-
this.phaseTimer = setInterval(() => {
|
|
201
|
-
this.phaseFrame = (this.phaseFrame + 1) % SPIN_DOTS.length;
|
|
202
|
-
this._renderPhase();
|
|
203
|
-
}, 80);
|
|
204
|
-
this._renderPhase();
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
updateProgress(done, total) {
|
|
208
|
-
this.phaseDone = done;
|
|
209
|
-
this.phaseTotal = total;
|
|
210
|
-
if (!READY) return;
|
|
211
|
-
this._renderProgress();
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
endPhase(name, ok = true) {
|
|
215
|
-
if (this.phaseTimer) { clearInterval(this.phaseTimer); this.phaseTimer = null; }
|
|
216
|
-
if (!READY) {
|
|
217
|
-
const icon = ok ? `${C.green}✓${A.reset}` : `${C.red}✗${A.reset}`;
|
|
218
|
-
console.log(` ${icon} ${name}`);
|
|
219
|
-
return;
|
|
220
|
-
}
|
|
221
|
-
const icon = ok ? `${C.green}✓${A.reset}` : `${C.red}✗${A.reset}`;
|
|
222
|
-
const label = `${icon} ${name}`;
|
|
223
|
-
// Write to rows 5 (phase) and 7 (progress) with result
|
|
224
|
-
const w = this._w;
|
|
225
|
-
const V = C.border;
|
|
226
|
-
const line = rpad(` ${label}`, w - 3);
|
|
227
|
-
this._write(
|
|
228
|
-
`${A.save}` +
|
|
229
|
-
this._at(5, 1) + A.clearLine +
|
|
230
|
-
`${V} ${line} ${V}${A.reset}` +
|
|
231
|
-
this._at(7, 1) + A.clearLine +
|
|
232
|
-
`${V} ${rpad('', w - 3)} ${V}${A.reset}` +
|
|
233
|
-
A.restore
|
|
234
|
-
);
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
flashEvent(type, msg) {
|
|
238
|
-
const now = new Date();
|
|
239
|
-
const ts = `${C.textDim}${String(now.getMinutes()).padStart(2,'0')}:${String(now.getSeconds()).padStart(2,'0')}${A.reset}`;
|
|
240
|
-
this.events.unshift({ ts, type, msg, id: Date.now() });
|
|
241
|
-
if (this.events.length > this.MAX_EVENTS) this.events.pop();
|
|
242
|
-
this.dirtyEvents = true;
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
setWorkers(workers) {
|
|
246
|
-
this.workers = workers;
|
|
247
|
-
this.dirtyWorkers = new Set(workers.map((_, i) => i));
|
|
248
|
-
this.dirtyStats = true;
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
markWorkerDirty(idx) {
|
|
252
|
-
this.dirtyWorkers = new Set(this.workers.map((_, i) => i));
|
|
253
|
-
this.dirtyStats = true;
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
setActive() {
|
|
257
|
-
if (this._active) return;
|
|
258
|
-
this._active = true;
|
|
259
|
-
|
|
260
|
-
if (READY) {
|
|
261
|
-
_captureActive = false;
|
|
262
|
-
if (_origWrite) { process.stdout.write = _origWrite; _origWrite = null; }
|
|
263
|
-
if (this._origLog) { console.log = this._origLog; this._origLog = null; }
|
|
264
|
-
_captureBuf = [];
|
|
265
|
-
|
|
266
|
-
this._write(A.eraseAll + A.hide);
|
|
267
|
-
this._drawLiveView();
|
|
268
|
-
this.dirtyWorkers.clear();
|
|
269
|
-
this.dirtyEvents = false;
|
|
270
|
-
this.dirtyStats = false;
|
|
271
|
-
|
|
272
|
-
// Start pulse animation for active status indicators
|
|
273
|
-
if (this._pulseTimer) clearInterval(this._pulseTimer);
|
|
274
|
-
this._pulseTimer = setInterval(() => {
|
|
275
|
-
this._pulseFrame = (this._pulseFrame + 1) % PULSE.length;
|
|
276
|
-
this.dirtyStats = true; // pulse affects account rows
|
|
277
|
-
}, 400);
|
|
278
|
-
|
|
279
|
-
this._startRenderLoop();
|
|
280
|
-
} else {
|
|
281
|
-
if (this._origLog) { console.log = this._origLog; this._origLog = null; }
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
shutdown(summary = {}) {
|
|
286
|
-
this._shutdown = true;
|
|
287
|
-
if (this._renderTimer) { clearInterval(this._renderTimer); this._renderTimer = null; }
|
|
288
|
-
if (this._pulseTimer) { clearInterval(this._pulseTimer); this._pulseTimer = null; }
|
|
289
|
-
if (this.phaseTimer) { clearInterval(this.phaseTimer); this.phaseTimer = null; }
|
|
290
|
-
|
|
291
|
-
if (READY && _origWrite) { process.stdout.write = _origWrite; _origWrite = null; }
|
|
292
|
-
if (this._origLog) { console.log = this._origLog; this._origLog = null; }
|
|
293
|
-
this._write(A.show);
|
|
294
|
-
|
|
295
|
-
const { totalCoins = 0, totalCmds = 0, totalSuccess = 0,
|
|
296
|
-
workers = [], uptime = 0, memMB = 0 } = summary;
|
|
297
|
-
|
|
298
|
-
const w = this._w;
|
|
299
|
-
let out = A.eraseAll;
|
|
300
|
-
|
|
301
|
-
// Top bar
|
|
302
|
-
out += this._boxTop();
|
|
303
|
-
out += this._statsBar();
|
|
304
|
-
out += this._sep();
|
|
305
|
-
|
|
306
|
-
// Column headers
|
|
307
|
-
out += `${this._row(4, this._colHdr())}`;
|
|
308
|
-
out += this._sep();
|
|
309
|
-
|
|
310
|
-
// Account rows
|
|
311
|
-
let row = 5;
|
|
312
|
-
for (let i = 0; i < workers.length && row < this._h - 4; i++) {
|
|
313
|
-
out += `${this._row(row++, this._accountLine(workers[i], i, workers))}`;
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
out += this._sep();
|
|
317
|
-
row++;
|
|
318
|
-
out += `${this._row(row++, this._totalLine(totalCoins, totalCmds, totalSuccess, uptime, memMB))}`;
|
|
319
|
-
out += this._boxBot();
|
|
320
|
-
|
|
321
|
-
this._write(out);
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
// ── Startup Screen ──────────────────────────────────────────────────────
|
|
325
|
-
|
|
326
|
-
_drawStartupScreen() {
|
|
327
|
-
const w = this._w;
|
|
328
|
-
let out = A.eraseAll;
|
|
329
|
-
|
|
330
|
-
// Top border
|
|
331
|
-
out += `${this._at(1,1)}${C.border}${TL}${'─'.repeat(w-2)}${TR}${A.reset}\n`;
|
|
332
|
-
|
|
333
|
-
// Version title
|
|
334
|
-
const title = ` ⬡ DANKGRINDER v${this._version || '?'} `;
|
|
335
|
-
out += `${this._at(2,1)}${C.border}${V} ${C.purple}${A.bold}${title}${rpad('', w - ansiLen(title) - 4)}${V}${A.reset}\n`;
|
|
336
|
-
|
|
337
|
-
// Subtitle with version info
|
|
338
|
-
const sub = `${C.textDim}24 commands · Auto-Recovery · Loss Limiter${A.reset}`;
|
|
339
|
-
out += `${this._at(3,1)}${C.border}${V} ${sub}${rpad('', w - ansiLen(sub) - 4)}${V}${A.reset}\n`;
|
|
340
|
-
|
|
341
|
-
out += `${this._at(4,1)}${C.border}${V}${'─'.repeat(w-2)}${V}${A.reset}\n`;
|
|
342
|
-
|
|
343
|
-
// Spinner + phase label (row 5)
|
|
344
|
-
out += `${this._at(5,1)}${C.border}${V}${rpad('', w-2)}${V}${A.reset}\n`;
|
|
345
|
-
|
|
346
|
-
// Progress bar (row 7)
|
|
347
|
-
out += `${this._at(7,1)}${C.border}${V}${rpad('', w-2)}${V}${A.reset}\n`;
|
|
348
|
-
|
|
349
|
-
// Checkmarks / status area (row 9)
|
|
350
|
-
out += `${this._at(9,1)}${C.border}${V}${rpad('', w-2)}${V}${A.reset}\n`;
|
|
351
|
-
|
|
352
|
-
// Spacer
|
|
353
|
-
out += `${this._at(11,1)}${C.border}${V}${rpad('', w-2)}${V}${A.reset}\n`;
|
|
354
|
-
|
|
355
|
-
// Bottom border
|
|
356
|
-
out += `${this._at(12,1)}${C.border}${BL}${'─'.repeat(w-2)}${BR}${A.reset}\n`;
|
|
357
|
-
|
|
358
|
-
// Footer
|
|
359
|
-
const hint = `${C.textDim}Initializing...${A.reset}`;
|
|
360
|
-
out += `${this._at(13,1)}${C.border}${V} ${hint}${rpad('', w - ansiLen(hint) - 4)}${V}${A.reset}\n`;
|
|
361
|
-
|
|
362
|
-
this._write(out);
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
_renderPhase() {
|
|
366
|
-
if (!READY || !this.phase) return;
|
|
367
|
-
const w = this._w;
|
|
368
|
-
const V = C.border;
|
|
369
|
-
const dot = SPIN_DOTS[this.phaseFrame];
|
|
370
|
-
const label = ` ${dot} ${this.phase} `;
|
|
371
|
-
const line = rpad(label, w - 3);
|
|
372
|
-
this._write(
|
|
373
|
-
`${A.save}` +
|
|
374
|
-
`${this._at(5,1)}${A.clearLine}` +
|
|
375
|
-
`${V} ${line} ${V}${A.reset}` +
|
|
376
|
-
A.restore
|
|
377
|
-
);
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
_renderProgress() {
|
|
381
|
-
if (!READY) return;
|
|
382
|
-
const w = this._w;
|
|
383
|
-
const V = C.border;
|
|
384
|
-
const { done, total } = { done: this.phaseDone, total: this.phaseTotal };
|
|
385
|
-
const barW = Math.max(16, w - 40);
|
|
386
|
-
const filled = total > 0 ? Math.round((done / total) * barW) : 0;
|
|
387
|
-
const block = SPIN_BLOCK[this.phaseFrame % SPIN_BLOCK.length];
|
|
388
|
-
|
|
389
|
-
// Filled bar with gradient: gold on left, purple on right
|
|
390
|
-
const filledPart = filled > 0
|
|
391
|
-
? `${C.gold}${'█'.repeat(Math.max(1, filled - 1))}${C.green}${block}${A.reset}`
|
|
392
|
-
: '';
|
|
393
|
-
const emptyPart = barW - filled > 0
|
|
394
|
-
? `${C.borderDim}${'░'.repeat(Math.max(0, barW - filled))}${A.reset}`
|
|
395
|
-
: '';
|
|
396
|
-
|
|
397
|
-
const pct = total > 0 ? `${Math.round((done / total) * 100)}%` : '';
|
|
398
|
-
const label = ` ${pct} `;
|
|
399
|
-
const bar = `${filledPart}${emptyPart}${C.textDim}${label}${A.reset}`;
|
|
400
|
-
const line = rpad(` ${bar}`, w - 3);
|
|
401
|
-
|
|
402
|
-
this._write(
|
|
403
|
-
`${A.save}` +
|
|
404
|
-
`${this._at(7,1)}${A.clearLine}` +
|
|
405
|
-
`${V} ${line} ${V}${A.reset}` +
|
|
406
|
-
A.restore
|
|
407
|
-
);
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
// ── Live View ─────────────────────────────────────────────────────────
|
|
411
|
-
|
|
412
|
-
_drawLiveView() {
|
|
413
|
-
let out = A.eraseAll;
|
|
414
|
-
out += this._boxTop();
|
|
415
|
-
out += this._statsBar();
|
|
416
|
-
out += this._sep();
|
|
417
|
-
out += `${this._row(4, this._colHdr())}`;
|
|
418
|
-
out += this._sep();
|
|
419
|
-
this._topRow = 5;
|
|
420
|
-
out += this._accountRows();
|
|
421
|
-
const sepRow = this._topRow + Math.min(this.windowSize, this.workers.length);
|
|
422
|
-
out += `${this._at(sepRow,1)}${C.border}${V}${'─'.repeat(this._w-2)}${V}${A.reset}\n`;
|
|
423
|
-
this._eventRow = sepRow + 1;
|
|
424
|
-
out += this._eventFeed();
|
|
425
|
-
out += this._boxBot();
|
|
426
|
-
this._write(out);
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
_boxTop() {
|
|
430
|
-
const w = this._w;
|
|
431
|
-
let o = '';
|
|
432
|
-
o += `${this._at(1,1)}${C.border}${TL}${'─'.repeat(w-2)}${TR}${A.reset}\n`;
|
|
433
|
-
const title = ` ⬡ DANKGRINDER v${this._version || '?'} `;
|
|
434
|
-
o += `${this._at(2,1)}${C.border}${V} ${C.purple}${A.bold}${title}${rpad('', w - ansiLen(title) - 4)}${V}${A.reset}\n`;
|
|
435
|
-
o += `${this._at(3,1)}${C.border}${V}${'─'.repeat(w-2)}${V}${A.reset}\n`;
|
|
436
|
-
return o;
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
_boxBot() {
|
|
440
|
-
const w = this._w;
|
|
441
|
-
const hint = `${C.textDim}↑↓ scroll Ctrl+C quit${A.reset}`;
|
|
442
|
-
let o = '';
|
|
443
|
-
o += `${this._at(this._footerRow,1)}${C.border}${BL}${'─'.repeat(w-2)}${BR}${A.reset}\n`;
|
|
444
|
-
o += `${this._at(this._footerRow+1,1)}${C.border}${V} ${hint}${rpad('', w - ansiLen(hint) - 4)}${V}${A.reset}\n`;
|
|
445
|
-
return o;
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
_sep() {
|
|
449
|
-
const w = this._w;
|
|
450
|
-
return `${C.border}${V}${'─'.repeat(w-2)}${V}${A.reset}\n`;
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
_row(r, content) {
|
|
454
|
-
const w = this._w;
|
|
455
|
-
return `${this._at(r,1)}${C.border}${V} ${content}${rpad('', w - ansiLen(content) - 4)} ${V}${A.reset}\n`;
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
_statsBar() {
|
|
459
|
-
const w = this._w;
|
|
460
|
-
const stats = this._buildStats();
|
|
461
|
-
return `${this._row(4, stats)}`;
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
_buildStats() {
|
|
465
|
-
let totalCoins = 0, totalCmds = 0, totalSuccess = 0, totalLs = 0;
|
|
466
|
-
let paused = 0, active = 0;
|
|
467
|
-
|
|
468
|
-
for (const wk of this.workers) {
|
|
469
|
-
totalCoins += wk.stats?.coins || 0;
|
|
470
|
-
totalCmds += wk.stats?.commands || 0;
|
|
471
|
-
totalSuccess += wk.stats?.successes|| 0;
|
|
472
|
-
if (wk._lifesavers != null) totalLs += wk._lifesavers;
|
|
473
|
-
if (wk.running && !wk._tokenInvalid) {
|
|
474
|
-
if (wk.paused || wk.dashboardPaused) paused++; else active++;
|
|
475
|
-
}
|
|
476
|
-
}
|
|
477
|
-
const uptime = this._fmtUptime(Date.now() - this._startTime);
|
|
478
|
-
const rate = totalCmds > 0 ? `${((totalSuccess / totalCmds) * 100).toFixed(0)}%` : '0%';
|
|
479
|
-
|
|
480
|
-
const items = [
|
|
481
|
-
[`⏱`, uptime, C.textDim],
|
|
482
|
-
[`⬡`, `${this.workers.length} accounts`, C.textDim],
|
|
483
|
-
[`⏣`, totalCoins.toLocaleString(), C.gold],
|
|
484
|
-
[`⚡`, `${totalCmds} cmds`, C.textDim],
|
|
485
|
-
[`📊`, `${rate} ok`, C.textDim],
|
|
486
|
-
[`♥`, `${totalLs}`, C.pink],
|
|
487
|
-
[`🟢`, `${active}`, C.green],
|
|
488
|
-
[`🔴`, `${paused}`, C.red],
|
|
489
|
-
];
|
|
490
|
-
|
|
491
|
-
return items.map(([icon, val, col]) =>
|
|
492
|
-
`${C.textDim}${icon}${A.reset} ${col}${val}${A.reset}`
|
|
493
|
-
).join(` ${C.borderDim}│${A.reset} `);
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
_colHdr() {
|
|
497
|
-
const cols = [
|
|
498
|
-
`${C.purple}#`,
|
|
499
|
-
`${C.purple}ACCOUNT`,
|
|
500
|
-
`${C.purple}COINS`,
|
|
501
|
-
`${C.purple}LV`,
|
|
502
|
-
`${C.purple}♥`,
|
|
503
|
-
`${C.purple}OK%`,
|
|
504
|
-
`${C.purple}STATUS`,
|
|
505
|
-
];
|
|
506
|
-
return cols.join(' ');
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
_accountRows() {
|
|
510
|
-
let out = '';
|
|
511
|
-
for (let i = 0; i < this.windowSize; i++) {
|
|
512
|
-
const wkIdx = this.windowStart + i;
|
|
513
|
-
const row = this._topRow + i;
|
|
514
|
-
if (row > this._h - 5) break;
|
|
515
|
-
if (wkIdx < this.workers.length) {
|
|
516
|
-
out += `${this._row(row, this._accountLine(this.workers[wkIdx], wkIdx, this.workers))}`;
|
|
517
|
-
} else {
|
|
518
|
-
out += `${this._row(row, '')}`;
|
|
519
|
-
}
|
|
520
|
-
}
|
|
521
|
-
return out;
|
|
522
|
-
}
|
|
523
|
-
|
|
524
|
-
_accountLine(wk, idx, workers) {
|
|
525
|
-
const rank = coinRank(wk, workers);
|
|
526
|
-
const pos = rank - 1; // 0-indexed
|
|
527
|
-
const isActive = wk.running && !wk._tokenInvalid && !wk.paused && !wk.dashboardPaused;
|
|
528
|
-
const ls = wk._lifesavers ?? '?';
|
|
529
|
-
const rate = wk.stats?.commands > 0
|
|
530
|
-
? `${((wk.stats.successes / wk.stats.commands) * 100).toFixed(0)}%`
|
|
531
|
-
: '0%';
|
|
532
|
-
|
|
533
|
-
// Rank badge
|
|
534
|
-
const medal = pos < 3 ? MEDALS[pos] : null;
|
|
535
|
-
const rankColor = pos === 0 ? C.rank1 : pos === 1 ? C.rank2 : pos === 2 ? C.rank3 : null;
|
|
536
|
-
const acctColor = C.ACCT[idx % C.ACCT.length];
|
|
537
|
-
|
|
538
|
-
// Color based on rank (top 3 get medal color, rest get account color)
|
|
539
|
-
const mainColor = isActive
|
|
540
|
-
? (rankColor || acctColor)
|
|
541
|
-
: C.textFaint;
|
|
542
|
-
|
|
543
|
-
const dimColor = isActive ? C.textDim : C.textFaint;
|
|
544
|
-
const goldColor = isActive ? C.gold : C.textDim;
|
|
545
|
-
const cyanColor = isActive ? C.cyan : C.textDim;
|
|
546
|
-
const lsColor = isActive
|
|
547
|
-
? (ls === 0 ? C.red : ls <= 2 ? C.orange : C.pink)
|
|
548
|
-
: C.textDim;
|
|
549
|
-
|
|
550
|
-
// Status with pulse for active
|
|
551
|
-
let dot, statusText;
|
|
552
|
-
if (!wk.running || wk._tokenInvalid) {
|
|
553
|
-
dot = `${C.textFaint}⚫${A.reset}`; statusText = `${C.textFaint}offline${A.reset}`;
|
|
554
|
-
} else if (wk.paused || wk.dashboardPaused) {
|
|
555
|
-
dot = `${C.red}🔴${A.reset}`; statusText = `${C.red}paused${A.reset}`;
|
|
556
|
-
} else {
|
|
557
|
-
const pulse = PULSE[this._pulseFrame];
|
|
558
|
-
dot = `${C.green}${pulse}${A.reset}`; statusText = `${C.green}active${A.reset}`;
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
// Account name — truncate with care
|
|
562
|
-
const rawName = wk.username || '?';
|
|
563
|
-
const nameDisplay = rawName.length > 20
|
|
564
|
-
? rawName.substring(0, 17) + '...'
|
|
565
|
-
: rawName;
|
|
566
|
-
|
|
567
|
-
// Coins display
|
|
568
|
-
const coins = (wk.stats?.coins || 0).toLocaleString();
|
|
569
|
-
const sign = (wk.stats?.coins || 0) >= 0 ? '+' : '';
|
|
570
|
-
const coinDisplay = `${goldColor}${sign}⏣${coins}${A.reset}`;
|
|
571
|
-
|
|
572
|
-
// Current command (minimal)
|
|
573
|
-
const cmd = (wk.lastStatus || '—').replace(/\x1b\[[0-9;]*m/g, '').substring(0, 14);
|
|
574
|
-
|
|
575
|
-
// Build rank badge
|
|
576
|
-
const rankBadge = medal
|
|
577
|
-
? `${rankColor}${A.bold}${medal}${A.reset}`
|
|
578
|
-
: `${dimColor}${rank}th${A.reset}`;
|
|
579
|
-
|
|
580
|
-
// Account name with subtle glow for top 3
|
|
581
|
-
const nameBadge = pos < 3
|
|
582
|
-
? `${mainColor}${A.bold}${nameDisplay}${A.reset}`
|
|
583
|
-
: `${mainColor}${nameDisplay}${A.reset}`;
|
|
584
|
-
|
|
585
|
-
const parts = [
|
|
586
|
-
rankBadge,
|
|
587
|
-
nameBadge,
|
|
588
|
-
coinDisplay,
|
|
589
|
-
`${cyanColor}Lv.${String(wk._level ?? '?').padStart(3)}${A.reset}`,
|
|
590
|
-
`${lsColor}♥${String(ls).padStart(2)}${A.reset}`,
|
|
591
|
-
`${dimColor}${rate.padStart(5)}${A.reset}`,
|
|
592
|
-
`${dot} ${statusText}`,
|
|
593
|
-
`${dimColor}${cmd}`,
|
|
594
|
-
];
|
|
595
|
-
|
|
596
|
-
return parts.join(' ');
|
|
597
|
-
}
|
|
598
|
-
|
|
599
|
-
_eventFeed() {
|
|
600
|
-
let out = '';
|
|
601
|
-
const visible = this.events.slice(0, Math.min(this.MAX_EVENTS, this._h - this._eventRow - 3));
|
|
602
|
-
for (let i = 0; i < visible.length; i++) {
|
|
603
|
-
const row = this._eventRow + i;
|
|
604
|
-
if (row > this._h - 3) break;
|
|
605
|
-
const e = visible[i];
|
|
606
|
-
const color = e.type === 'death' ? C.red
|
|
607
|
-
: e.type === 'lowls' ? C.orange
|
|
608
|
-
: e.type === 'levelup'? C.cyan
|
|
609
|
-
: e.type === 'success'? C.green
|
|
610
|
-
: C.textDim;
|
|
611
|
-
out += this._row(row, ` ${e.ts} ${color}${e.msg}${A.reset}`);
|
|
612
|
-
}
|
|
613
|
-
this._footerRow = this._eventRow + visible.length;
|
|
614
|
-
return out;
|
|
615
|
-
}
|
|
616
|
-
|
|
617
|
-
_totalLine(totalCoins, totalCmds, totalSuccess, uptime, memMB) {
|
|
618
|
-
const rate = totalCmds > 0 ? `${((totalSuccess / totalCmds) * 100).toFixed(0)}%` : '0%';
|
|
619
|
-
const items = [
|
|
620
|
-
[`💰`, `${C.gold}${A.bold}TOTAL:${A.reset}`, `${C.gold}${A.bold}⏣${totalCoins.toLocaleString()}${A.reset}`],
|
|
621
|
-
[`⚡`, `${C.textDim}${totalCmds} cmds${A.reset}`, `${C.textDim}${rate} ok${A.reset}`],
|
|
622
|
-
[`⏱`, `${C.textDim}${this._fmtUptime(uptime)}${A.reset}`, `${C.textDim}${memMB}MB${A.reset}`],
|
|
623
|
-
];
|
|
624
|
-
return items.map(([, label, val]) => `${label} ${val}`).join(' ');
|
|
625
|
-
}
|
|
626
|
-
|
|
627
|
-
// ── Render loop ───────────────────────────────────────────────────────
|
|
628
|
-
|
|
629
|
-
_render() {
|
|
630
|
-
if (!READY || this._shutdown || !this._active) return;
|
|
631
|
-
|
|
632
|
-
if (this.dirtyStats) {
|
|
633
|
-
// Update stats bar (row 4)
|
|
634
|
-
const stats = this._buildStats();
|
|
635
|
-
const w = this._w;
|
|
636
|
-
const V = C.border;
|
|
637
|
-
this._write(
|
|
638
|
-
`${this._at(4,1)}${V} ${rpad(stats, w-4)} ${V}${A.reset}`
|
|
639
|
-
);
|
|
640
|
-
// Re-draw account rows to update pulse/status
|
|
641
|
-
for (let i = 0; i < this.windowSize; i++) {
|
|
642
|
-
const wkIdx = this.windowStart + i;
|
|
643
|
-
const row = this._topRow + i;
|
|
644
|
-
if (row > this._h - 5) break;
|
|
645
|
-
if (wkIdx < this.workers.length) {
|
|
646
|
-
const line = this._accountLine(this.workers[wkIdx], wkIdx, this.workers);
|
|
647
|
-
this._write(`${this._at(row,1)}${C.border}${V} ${rpad(line, w-4)} ${V}${A.reset}`);
|
|
648
|
-
}
|
|
649
|
-
}
|
|
650
|
-
this.dirtyStats = false;
|
|
651
|
-
}
|
|
652
|
-
|
|
653
|
-
if (this.dirtyWorkers.size > 0) {
|
|
654
|
-
const w = this._w;
|
|
655
|
-
const V = C.border;
|
|
656
|
-
for (const wkIdx of this.dirtyWorkers) {
|
|
657
|
-
const localRow = wkIdx - this.windowStart;
|
|
658
|
-
const row = this._topRow + localRow;
|
|
659
|
-
if (row < this._topRow || row > this._h - 5) continue;
|
|
660
|
-
if (wkIdx < this.workers.length) {
|
|
661
|
-
const line = this._accountLine(this.workers[wkIdx], wkIdx, this.workers);
|
|
662
|
-
this._write(`${this._at(row,1)}${C.border}${V} ${rpad(line, w-4)} ${V}${A.reset}`);
|
|
663
|
-
} else {
|
|
664
|
-
this._write(`${this._at(row,1)}${C.border}${V}${rpad('', w-2)}${V}${A.reset}`);
|
|
665
|
-
}
|
|
666
|
-
}
|
|
667
|
-
}
|
|
668
|
-
|
|
669
|
-
if (this.dirtyEvents) {
|
|
670
|
-
this._write(this._eventFeed());
|
|
671
|
-
}
|
|
672
|
-
|
|
673
|
-
this.dirtyWorkers.clear();
|
|
674
|
-
this.dirtyEvents = false;
|
|
675
|
-
}
|
|
676
|
-
|
|
677
|
-
_startRenderLoop() {
|
|
678
|
-
if (this._renderTimer) clearInterval(this._renderTimer);
|
|
679
|
-
this._renderTimer = setInterval(() => this._render(), 250);
|
|
680
|
-
}
|
|
681
|
-
|
|
682
|
-
// ── Internals ──────────────────────────────────────────────────────────
|
|
683
|
-
|
|
684
|
-
_updateSize() {
|
|
685
|
-
try {
|
|
686
|
-
this._w = process.stdout.columns || 110;
|
|
687
|
-
this._h = process.stdout.rows || 35;
|
|
688
|
-
this.windowSize = Math.max(4, this._h - 11);
|
|
689
|
-
} catch (_) {
|
|
690
|
-
this._w = 110; this._h = 35; this.windowSize = 17;
|
|
691
|
-
}
|
|
692
|
-
}
|
|
693
|
-
|
|
694
|
-
_onResize() {
|
|
695
|
-
clearTimeout(this._resizeTimer);
|
|
696
|
-
this._resizeTimer = setTimeout(() => {
|
|
697
|
-
this._updateSize();
|
|
698
|
-
if (this._active) {
|
|
699
|
-
this._write(A.eraseAll);
|
|
700
|
-
this._drawLiveView();
|
|
701
|
-
}
|
|
702
|
-
}, 100);
|
|
703
|
-
}
|
|
704
|
-
|
|
705
|
-
_at(r, c) { return `\x1b[${r};${c}H`; }
|
|
706
|
-
_write(s) { if (s) process.stdout.write(s); }
|
|
707
|
-
|
|
708
|
-
_fmtUptime(ms) {
|
|
709
|
-
if (!ms) return '0s';
|
|
710
|
-
const s = Math.floor(ms / 1000);
|
|
711
|
-
if (s < 60) return `${s}s`;
|
|
712
|
-
const m = Math.floor(s / 60);
|
|
713
|
-
if (m < 60) return `${m}m ${s%60}s`;
|
|
714
|
-
const h = Math.floor(m / 60);
|
|
715
|
-
if (h < 24) return `${h}h ${m%60}m`;
|
|
716
|
-
return `${Math.floor(h/24)}d ${h%24}h`;
|
|
717
|
-
}
|
|
718
|
-
}
|
|
719
|
-
|
|
720
|
-
module.exports = new Terminal();
|