dankgrinder 6.37.0 → 6.39.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/commands/farm.js +36 -10
- package/lib/commands/stream.js +1 -1
- package/lib/commands/work.js +1 -1
- package/lib/grinder.js +167 -22
- package/package.json +1 -1
package/lib/commands/farm.js
CHANGED
|
@@ -68,11 +68,30 @@ function parseFarmCooldownSec(text) {
|
|
|
68
68
|
const hasCooldownContext = /easy tiger|slow it down|already farmed|farm again|cooldown|can be used again|on cooldown/.test(lower);
|
|
69
69
|
if (!hasCooldownContext) return null;
|
|
70
70
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
71
|
+
// Handle all Discord timestamp formats: :R, :f, :F, :T, :t, :d, :D
|
|
72
|
+
// :R = relative seconds from now (already seconds since epoch difference)
|
|
73
|
+
// :f/:F = absolute datetime → diff from now
|
|
74
|
+
// :T/:t/:d/:D = not useful for cooldowns (no time component)
|
|
75
|
+
const RE_TS_ALL = /<t:(\d+):([tTdDfFR])>/g;
|
|
76
|
+
const now = Math.floor(Date.now() / 1000);
|
|
77
|
+
let best = null;
|
|
78
|
+
for (const m of clean.matchAll(RE_TS_ALL)) {
|
|
79
|
+
const ts = parseInt(m[1], 10);
|
|
80
|
+
const fmt = m[2];
|
|
81
|
+
if (!Number.isFinite(ts)) continue;
|
|
82
|
+
let diff;
|
|
83
|
+
if (fmt === 'R') {
|
|
84
|
+
diff = ts - now; // :R is already relative seconds
|
|
85
|
+
} else if (fmt === 'f' || fmt === 'F' || fmt === 'T') {
|
|
86
|
+
diff = ts - now; // :f/:F/:T are absolute → diff gives seconds remaining
|
|
87
|
+
} else {
|
|
88
|
+
// :t/:d/:D don't have enough info for cooldown calc — skip
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
if (diff > 0 && (best === null || diff < best)) best = diff;
|
|
75
92
|
}
|
|
93
|
+
if (best !== null) return Math.max(5, best);
|
|
94
|
+
|
|
76
95
|
const mm = clean.match(RE_MIN);
|
|
77
96
|
if (mm) return parseInt(mm[1], 10) * 60;
|
|
78
97
|
const hh = clean.match(RE_HR);
|
|
@@ -1589,16 +1608,23 @@ async function runFarm({ channel, waitForDankMemer, client, redis, accountId })
|
|
|
1589
1608
|
LOG.coin(`[farm] ${c.green}+⏣ ${coins.toLocaleString()}${c.reset}`);
|
|
1590
1609
|
// After a harvest, Dank Memer auto-plants new crops. Record harvest time
|
|
1591
1610
|
// in Redis so the next run knows when crops should be ready.
|
|
1592
|
-
//
|
|
1593
|
-
//
|
|
1611
|
+
// Only force a short 30s re-check if there is NO known grow queue timer.
|
|
1612
|
+
// If growReadyEnd is set (e.g. "ready in 26m"), that takes priority.
|
|
1594
1613
|
const afterHarvestState = analyzeFarmState({ msg: cycleResponse, text });
|
|
1595
1614
|
const farmIsHarvested = afterHarvestState.stage === 'overview'
|
|
1596
1615
|
|| /seems? pretty empty|empty\.{0,3}|hoe|water|plant|harvest/i.test(text);
|
|
1597
1616
|
if (farmIsHarvested) {
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1617
|
+
const hasGrowTimer = Number.isFinite(growReadyEnd) && growReadyEnd > 0;
|
|
1618
|
+
if (hasGrowTimer) {
|
|
1619
|
+
// Crops were planted and a grow queue timer exists (e.g. "ready in 26m").
|
|
1620
|
+
// Respect that timer — the 30s cap should NOT override it.
|
|
1621
|
+
LOG.info(`[farm] harvest complete (+⏣ ${coins.toLocaleString()}), farm is ${afterHarvestState.stage}, grow in ${Math.ceil(growReadyEnd / 60)}m — waiting for grow queue`);
|
|
1622
|
+
} else {
|
|
1623
|
+
// No grow queue timer visible (farm is truly empty or manage menu without timestamp).
|
|
1624
|
+
// Short re-check to start the next hoe→water→plant→harvest cycle.
|
|
1625
|
+
nextCd = Math.min(nextCd, 30);
|
|
1626
|
+
LOG.info(`[farm] harvest complete (+⏣ ${coins.toLocaleString()}), farm is ${afterHarvestState.stage} — re-checking in 30s`);
|
|
1627
|
+
}
|
|
1602
1628
|
}
|
|
1603
1629
|
// Record in Redis for cross-instance awareness
|
|
1604
1630
|
let growDurMs = null;
|
package/lib/commands/stream.js
CHANGED
|
@@ -206,7 +206,7 @@ async function runStream({ channel, waitForDankMemer, client }) {
|
|
|
206
206
|
|
|
207
207
|
if (isHoldTight(response)) {
|
|
208
208
|
const reason = getHoldTightReason(response);
|
|
209
|
-
return { result: `hold tight (${reason || 'unknown'})`, coins: 0, holdTightReason: reason };
|
|
209
|
+
return { result: `hold tight (${reason || 'unknown'})`, coins: 0, holdTightReason: reason, nextCooldownSec: 30 };
|
|
210
210
|
}
|
|
211
211
|
|
|
212
212
|
await hydrate(response);
|
package/lib/commands/work.js
CHANGED
|
@@ -418,7 +418,7 @@ async function runWorkShift({ channel, waitForDankMemer }) {
|
|
|
418
418
|
const reason = getHoldTightReason(current);
|
|
419
419
|
LOG.warn(`[work] Hold Tight${reason ? ` (reason: /${reason})` : ''} — waiting 30s`);
|
|
420
420
|
await sleep(30000);
|
|
421
|
-
return { result: `hold tight (${reason || 'unknown'})`, coins: 0, holdTightReason: reason };
|
|
421
|
+
return { result: `hold tight (${reason || 'unknown'})`, coins: 0, holdTightReason: reason, nextCooldownSec: 30 };
|
|
422
422
|
}
|
|
423
423
|
|
|
424
424
|
if (isCV2(current)) await ensureCV2(current);
|
package/lib/grinder.js
CHANGED
|
@@ -94,6 +94,8 @@ const c = {
|
|
|
94
94
|
};
|
|
95
95
|
|
|
96
96
|
const WORKER_COLORS = [c.cyan, c.magenta, c.yellow, c.green, c.blue, c.red];
|
|
97
|
+
// Unique marker written to stdout so we can query cursor position via DSR response
|
|
98
|
+
const MARKER = '\x1b[6n\x1b[@@MARKER@@';
|
|
97
99
|
const DANK_MEMER_ID = '270904126974590976';
|
|
98
100
|
|
|
99
101
|
// ── Safe options for search/crime ──────────────────────────
|
|
@@ -1110,6 +1112,11 @@ class AccountWorker {
|
|
|
1110
1112
|
this.commandQueue = null;
|
|
1111
1113
|
this.lastHealthCheck = Date.now();
|
|
1112
1114
|
this.doneToday = new Map();
|
|
1115
|
+
|
|
1116
|
+
// Dynamic cooldown learning: tracks actual parsed cooldowns per command.
|
|
1117
|
+
// Persisted in Redis hash `dkg:cd:learned:{accountId}` for cross-restart persistence.
|
|
1118
|
+
// Used as adaptive floor when command parsers don't return exact cooldowns.
|
|
1119
|
+
this._learnedCooldowns = new Map();
|
|
1113
1120
|
this._fishRoundsSinceSell = 0;
|
|
1114
1121
|
this._autoDepositThreshold = account.auto_deposit_threshold || 500000;
|
|
1115
1122
|
|
|
@@ -1995,6 +2002,11 @@ class AccountWorker {
|
|
|
1995
2002
|
if (cmdResult.nextCooldownSec) {
|
|
1996
2003
|
await this.setCooldown(cmdName, cmdResult.nextCooldownSec);
|
|
1997
2004
|
this._lastCooldownOverride = cmdResult.nextCooldownSec;
|
|
2005
|
+
// Learn: record this cooldown as the known value for future fallback use
|
|
2006
|
+
this._learnedCooldowns.set(cmdName, cmdResult.nextCooldownSec);
|
|
2007
|
+
if (redis) {
|
|
2008
|
+
await redis.hset(`dkg:cd:learned:${this.account.id}`, cmdName, String(cmdResult.nextCooldownSec));
|
|
2009
|
+
}
|
|
1998
2010
|
}
|
|
1999
2011
|
|
|
2000
2012
|
// Smart gambling loss tracker
|
|
@@ -2115,6 +2127,25 @@ class AccountWorker {
|
|
|
2115
2127
|
}
|
|
2116
2128
|
}
|
|
2117
2129
|
|
|
2130
|
+
// Load previously learned cooldowns from Redis so floors adapt across restarts.
|
|
2131
|
+
async _loadLearnedCooldowns() {
|
|
2132
|
+
if (!redis) return;
|
|
2133
|
+
try {
|
|
2134
|
+
const learned = await redis.hgetall(`dkg:cd:learned:${this.account.id}`);
|
|
2135
|
+
for (const [cmd, val] of Object.entries(learned)) {
|
|
2136
|
+
const n = parseFloat(val);
|
|
2137
|
+
if (Number.isFinite(n) && n > 0) {
|
|
2138
|
+
this._learnedCooldowns.set(cmd, n);
|
|
2139
|
+
}
|
|
2140
|
+
}
|
|
2141
|
+
if (this._learnedCooldowns.size > 0) {
|
|
2142
|
+
this.log('info', `Loaded ${this._learnedCooldowns.size} learned cooldowns from Redis`);
|
|
2143
|
+
}
|
|
2144
|
+
} catch (e) {
|
|
2145
|
+
this.log('warn', `Failed to load learned cooldowns: ${e.message}`);
|
|
2146
|
+
}
|
|
2147
|
+
}
|
|
2148
|
+
|
|
2118
2149
|
printStats() {
|
|
2119
2150
|
// Stats are shown in the live dashboard, no-op here
|
|
2120
2151
|
}
|
|
@@ -2166,7 +2197,7 @@ class AccountWorker {
|
|
|
2166
2197
|
// Alert is NOT scheduled — it's reactive (listener-based, see grindLoop)
|
|
2167
2198
|
].map(Object.freeze);
|
|
2168
2199
|
|
|
2169
|
-
buildCommandQueue() {
|
|
2200
|
+
async buildCommandQueue() {
|
|
2170
2201
|
const heap = new MinHeap();
|
|
2171
2202
|
const now = Date.now();
|
|
2172
2203
|
let enabled = AccountWorker.COMMAND_MAP.filter(
|
|
@@ -2180,8 +2211,33 @@ class AccountWorker {
|
|
|
2180
2211
|
enabled = AccountWorker.COMMAND_MAP.filter(ci => Boolean(this.account[ci.key]));
|
|
2181
2212
|
}
|
|
2182
2213
|
|
|
2214
|
+
// Restore cooldown state from Redis so commands don't re-run immediately
|
|
2215
|
+
// after restart. We use TTL to calculate remaining cooldown time.
|
|
2216
|
+
const accountId = this.account.id;
|
|
2217
|
+
const cmdKeys = enabled.map(info => `dkg:cd:${accountId}:${info.cmd}`);
|
|
2218
|
+
let ttlMap = new Map();
|
|
2219
|
+
if (redis) {
|
|
2220
|
+
const pipeline = redis.pipeline();
|
|
2221
|
+
for (const k of cmdKeys) pipeline.ttl(k);
|
|
2222
|
+
const results = await pipeline.exec();
|
|
2223
|
+
for (let i = 0; i < cmdKeys.length; i++) {
|
|
2224
|
+
const [err, val] = results[i];
|
|
2225
|
+
if (!err && Number.isFinite(val) && val > 0) {
|
|
2226
|
+
ttlMap.set(cmdKeys[i], val);
|
|
2227
|
+
}
|
|
2228
|
+
}
|
|
2229
|
+
}
|
|
2230
|
+
|
|
2183
2231
|
for (const info of enabled) {
|
|
2184
|
-
|
|
2232
|
+
let nextRunAt = now;
|
|
2233
|
+
const key = `dkg:cd:${accountId}:${info.cmd}`;
|
|
2234
|
+
const ttlVal = ttlMap.get(key);
|
|
2235
|
+
if (Number.isFinite(ttlVal) && ttlVal > 0) {
|
|
2236
|
+
nextRunAt = now + ttlVal * 1000;
|
|
2237
|
+
this._cooldownBloom.add(key);
|
|
2238
|
+
this.log('info', `Restored cooldown for ${info.cmd}: ${ttlVal}s remaining`);
|
|
2239
|
+
}
|
|
2240
|
+
heap.push({ cmd: info.cmd, nextRunAt, priority: info.priority, info });
|
|
2185
2241
|
}
|
|
2186
2242
|
return heap;
|
|
2187
2243
|
}
|
|
@@ -2309,7 +2365,7 @@ class AccountWorker {
|
|
|
2309
2365
|
|
|
2310
2366
|
// Resume grind loop if it was running
|
|
2311
2367
|
if (!this.commandQueue || this.commandQueue.size === 0) {
|
|
2312
|
-
this.commandQueue = this.buildCommandQueue();
|
|
2368
|
+
this.commandQueue = await this.buildCommandQueue();
|
|
2313
2369
|
}
|
|
2314
2370
|
} else {
|
|
2315
2371
|
this.log('error', 'Recovered connection but channel not found — retrying');
|
|
@@ -2407,7 +2463,7 @@ class AccountWorker {
|
|
|
2407
2463
|
}
|
|
2408
2464
|
|
|
2409
2465
|
if (!this.commandQueue || this.commandQueue.size === 0) {
|
|
2410
|
-
this.commandQueue = this.buildCommandQueue();
|
|
2466
|
+
this.commandQueue = await this.buildCommandQueue();
|
|
2411
2467
|
}
|
|
2412
2468
|
if (!this.commandQueue || this.commandQueue.size === 0) {
|
|
2413
2469
|
this.tickTimeout = setTimeout(() => this.tick(), 15000);
|
|
@@ -2558,22 +2614,33 @@ class AccountWorker {
|
|
|
2558
2614
|
|
|
2559
2615
|
if (this.commandQueue && this.running && !shutdownCalled) {
|
|
2560
2616
|
const hasOverride = Number.isFinite(this._lastCooldownOverride) && this._lastCooldownOverride > 0;
|
|
2561
|
-
let effectiveWait = hasOverride ? this._lastCooldownOverride : totalWait;
|
|
2562
2617
|
this._lastCooldownOverride = null;
|
|
2563
2618
|
|
|
2564
|
-
|
|
2565
|
-
if (
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2619
|
+
let scheduledWaitSec;
|
|
2620
|
+
if (hasOverride) {
|
|
2621
|
+
// Exact cooldown returned by command parser — use it without jitter or backoff.
|
|
2622
|
+
// This ensures work (1h), adventure (5h), stream (10m), farm (varies) are honored exactly.
|
|
2623
|
+
scheduledWaitSec = Math.max(1, this._lastCooldownOverride);
|
|
2624
|
+
} else {
|
|
2625
|
+
// Jitter-based cooldown for commands without a parsed override.
|
|
2626
|
+
let effectiveWait = totalWait;
|
|
2627
|
+
|
|
2628
|
+
// Smart fallback floors: static floor OR dynamically learned cooldown (whichever is higher).
|
|
2629
|
+
// Learned cooldowns come from actual parsed responses and persist across restarts via Redis.
|
|
2630
|
+
const staticFloor = AccountWorker.SMART_CD_FLOORS[item.cmd] || 0;
|
|
2631
|
+
const learnedCd = this._learnedCooldowns.get(item.cmd) || 0;
|
|
2632
|
+
const adaptiveFloor = Math.max(staticFloor, learnedCd);
|
|
2633
|
+
if (adaptiveFloor > 0) {
|
|
2634
|
+
effectiveWait = Math.max(effectiveWait, adaptiveFloor);
|
|
2569
2635
|
}
|
|
2570
|
-
}
|
|
2571
2636
|
|
|
2572
|
-
|
|
2573
|
-
|
|
2637
|
+
if (earned <= 0 && !noFailCmds.includes(item.cmd) && effectiveWait < MIN_FAIL_COOLDOWN) {
|
|
2638
|
+
effectiveWait = MIN_FAIL_COOLDOWN;
|
|
2639
|
+
}
|
|
2640
|
+
|
|
2641
|
+
scheduledWaitSec = Math.max(1, effectiveWait * backoffMultiplier);
|
|
2574
2642
|
}
|
|
2575
2643
|
|
|
2576
|
-
const scheduledWaitSec = Math.max(1, effectiveWait * backoffMultiplier);
|
|
2577
2644
|
await this.setCooldown(item.cmd, scheduledWaitSec);
|
|
2578
2645
|
item.nextRunAt = Date.now() + scheduledWaitSec * 1000;
|
|
2579
2646
|
this.commandQueue.push(item);
|
|
@@ -2611,7 +2678,8 @@ class AccountWorker {
|
|
|
2611
2678
|
this.failStreak = 0;
|
|
2612
2679
|
this.cycleCount = 0;
|
|
2613
2680
|
this.lastCommandRun = 0;
|
|
2614
|
-
|
|
2681
|
+
await this._loadLearnedCooldowns();
|
|
2682
|
+
this.commandQueue = await this.buildCommandQueue();
|
|
2615
2683
|
this.lastHealthCheck = Date.now();
|
|
2616
2684
|
|
|
2617
2685
|
// Reactive alert listener: run `pls alert` only when Dank Memer
|
|
@@ -3048,9 +3116,30 @@ async function start(apiKey, apiUrl) {
|
|
|
3048
3116
|
}
|
|
3049
3117
|
loginLines.push(` ${'─'.repeat(loginVis)}`);
|
|
3050
3118
|
for (const l of loginLines) console.log(l);
|
|
3051
|
-
|
|
3119
|
+
|
|
3120
|
+
// Dynamically capture the starting row of the login table via DSR
|
|
3121
|
+
let loginBaseRow = 1;
|
|
3122
|
+
const captureLoginRow = () => new Promise(resolve => {
|
|
3123
|
+
process.stdout.write(MARKER);
|
|
3124
|
+
const chunks = [];
|
|
3125
|
+
const handler = (chunk) => {
|
|
3126
|
+
chunks.push(chunk);
|
|
3127
|
+
const raw = chunks.join('');
|
|
3128
|
+
const m = raw.match(/\x1b\[(\d+);\d+R/);
|
|
3129
|
+
if (m) {
|
|
3130
|
+
process.stdin.removeListener('data', handler);
|
|
3131
|
+
loginBaseRow = parseInt(m[1], 10) + 1;
|
|
3132
|
+
resolve();
|
|
3133
|
+
}
|
|
3134
|
+
};
|
|
3135
|
+
process.stdin.on('data', handler);
|
|
3136
|
+
setTimeout(resolve, 50);
|
|
3137
|
+
});
|
|
3138
|
+
await captureLoginRow();
|
|
3052
3139
|
|
|
3053
3140
|
let loginPending = new Array(accounts.length).fill(true);
|
|
3141
|
+
const moveToRow = (row) => process.stdout.write(`\x1b[${row};1H`);
|
|
3142
|
+
|
|
3054
3143
|
const drawLoginSpinners = () => {
|
|
3055
3144
|
for (let i = 0; i < loginPending.length; i++) {
|
|
3056
3145
|
if (!loginPending[i]) continue;
|
|
@@ -3059,8 +3148,13 @@ async function start(apiKey, apiUrl) {
|
|
|
3059
3148
|
const name = loginStates[i].name.substring(0, colName).padEnd(colName);
|
|
3060
3149
|
const guild = c.dim + 'logging in...'.substring(0, colGuild) + c.reset;
|
|
3061
3150
|
const cmds = c.dim + '···'.padEnd(colCmds) + c.reset;
|
|
3062
|
-
|
|
3151
|
+
const row = loginBaseRow + 1 + i; // +1 skips the top border line
|
|
3152
|
+
moveToRow(row);
|
|
3153
|
+
process.stdout.write(` ${num} ${rgb(139, 92, 246)}${spin}${c.reset} ${name} ${guild} ${cmds}\x1b[K`);
|
|
3063
3154
|
}
|
|
3155
|
+
// Move cursor back to bottom to avoid overwriting the bottom border
|
|
3156
|
+
const lastRow = loginBaseRow + 1 + accounts.length + 1;
|
|
3157
|
+
moveToRow(lastRow);
|
|
3064
3158
|
};
|
|
3065
3159
|
const loginSpinnerInterval = setInterval(drawLoginSpinners, 80);
|
|
3066
3160
|
|
|
@@ -3089,7 +3183,9 @@ async function start(apiKey, apiUrl) {
|
|
|
3089
3183
|
guild = 'timeout'.padEnd(colGuild);
|
|
3090
3184
|
cmds = '···'.padEnd(colCmds);
|
|
3091
3185
|
}
|
|
3092
|
-
|
|
3186
|
+
const row = loginBaseRow + 1 + idx; // +1 skips the top border line
|
|
3187
|
+
moveToRow(row);
|
|
3188
|
+
process.stdout.write(` ${num} ${sts} ${name} ${c.dim}${guild}${c.reset} ${c.dim}${cmds}${c.reset}\x1b[K`);
|
|
3093
3189
|
};
|
|
3094
3190
|
|
|
3095
3191
|
const parsedGapMin = Number.parseInt(String(process.env.LOGIN_GAP_MIN_MS || '50'), 10);
|
|
@@ -3137,6 +3233,28 @@ async function start(apiKey, apiUrl) {
|
|
|
3137
3233
|
const iColVal = 16;
|
|
3138
3234
|
const invVis = 7 + iColNum + iColName + iColItems + iColVal + 12;
|
|
3139
3235
|
|
|
3236
|
+
// Print a unique marker, query its position, then overwrite it with the table
|
|
3237
|
+
process.stdout.write(MARKER);
|
|
3238
|
+
let invBaseRow = 1;
|
|
3239
|
+
const captureRow = () => new Promise(resolve => {
|
|
3240
|
+
const chunks = [];
|
|
3241
|
+
const handler = (chunk) => {
|
|
3242
|
+
chunks.push(chunk);
|
|
3243
|
+
const raw = chunks.join('');
|
|
3244
|
+
const m = raw.match(/\x1b\[(\d+);\d+R/);
|
|
3245
|
+
if (m) {
|
|
3246
|
+
process.stdin.removeListener('data', handler);
|
|
3247
|
+
invBaseRow = parseInt(m[1], 10) + 1; // +1: first account row is after marker
|
|
3248
|
+
resolve();
|
|
3249
|
+
}
|
|
3250
|
+
};
|
|
3251
|
+
process.stdin.on('data', handler);
|
|
3252
|
+
setTimeout(resolve, 50);
|
|
3253
|
+
});
|
|
3254
|
+
await captureRow();
|
|
3255
|
+
|
|
3256
|
+
// Now print the inventory table starting at invBaseRow
|
|
3257
|
+
const invMoveToRow = (row) => process.stdout.write(`\x1b[${row};1H`);
|
|
3140
3258
|
console.log(` ${'─'.repeat(invVis)}`);
|
|
3141
3259
|
for (let i = 0; i < activeWorkers.length; i++) {
|
|
3142
3260
|
const w = activeWorkers[i];
|
|
@@ -3155,7 +3273,8 @@ async function start(apiKey, apiUrl) {
|
|
|
3155
3273
|
const filled = Math.round(pct * barW);
|
|
3156
3274
|
const bar = rgb(34, 211, 238) + '█'.repeat(filled) + rgb(50, 50, 70) + '░'.repeat(barW - filled) + c.reset;
|
|
3157
3275
|
const pctStr = `${Math.round(pct * 100)}%`;
|
|
3158
|
-
|
|
3276
|
+
invMoveToRow(invBaseRow);
|
|
3277
|
+
process.stdout.write(` ${rgb(34, 211, 238)}${spin}${c.reset} ${c.dim}Inventory...${c.reset} ${bar} ${c.bold}${rgb(52, 211, 153)}${activeWorkers.length - invPending}${c.reset}${c.dim}/${c.reset}${c.white}${activeWorkers.length}${c.reset} ${c.dim}${pctStr}${c.reset} \x1b[K`);
|
|
3159
3278
|
};
|
|
3160
3279
|
const invSpinnerInterval = setInterval(drawInvProgress, 80);
|
|
3161
3280
|
|
|
@@ -3171,7 +3290,9 @@ async function start(apiKey, apiUrl) {
|
|
|
3171
3290
|
const sts = invRes?.ok ? `${rgb(52, 211, 153)}✓${c.reset}` : `${rgb(239, 68, 68)}✗${c.reset}`;
|
|
3172
3291
|
const itemStr = `${items}`.padEnd(iColItems);
|
|
3173
3292
|
const valStr = invRes?.ok ? `${c.green}⏣${val.toLocaleString()}${c.reset}` : `${c.dim}···${c.reset}`;
|
|
3174
|
-
|
|
3293
|
+
const row = invBaseRow + 1 + i;
|
|
3294
|
+
invMoveToRow(row);
|
|
3295
|
+
process.stdout.write(` ${num} ${sts} ${name} ${itemStr} ${valStr.padEnd(iColVal + 5)}\x1b[K`);
|
|
3175
3296
|
if (invRes?.ok) invDone++; else invFailed++;
|
|
3176
3297
|
}));
|
|
3177
3298
|
|
|
@@ -3195,6 +3316,27 @@ async function start(apiKey, apiUrl) {
|
|
|
3195
3316
|
const bColLs = 4;
|
|
3196
3317
|
const balVis = 7 + bColNum + bColName + bColWallet + bColBank + bColTotal + bColLs + 14;
|
|
3197
3318
|
|
|
3319
|
+
// Capture starting row for balance phase
|
|
3320
|
+
process.stdout.write(MARKER);
|
|
3321
|
+
let balBaseRow = 1;
|
|
3322
|
+
const balCaptureRow = () => new Promise(resolve => {
|
|
3323
|
+
const chunks = [];
|
|
3324
|
+
const handler = (chunk) => {
|
|
3325
|
+
chunks.push(chunk);
|
|
3326
|
+
const raw = chunks.join('');
|
|
3327
|
+
const m = raw.match(/\x1b\[(\d+);\d+R/);
|
|
3328
|
+
if (m) {
|
|
3329
|
+
process.stdin.removeListener('data', handler);
|
|
3330
|
+
balBaseRow = parseInt(m[1], 10) + 1;
|
|
3331
|
+
resolve();
|
|
3332
|
+
}
|
|
3333
|
+
};
|
|
3334
|
+
process.stdin.on('data', handler);
|
|
3335
|
+
setTimeout(resolve, 50);
|
|
3336
|
+
});
|
|
3337
|
+
await balCaptureRow();
|
|
3338
|
+
|
|
3339
|
+
const balMoveToRow = (row) => process.stdout.write(`\x1b[${row};1H`);
|
|
3198
3340
|
console.log(` ${'─'.repeat(balVis)}`);
|
|
3199
3341
|
for (let i = 0; i < activeWorkers.length; i++) {
|
|
3200
3342
|
const w = activeWorkers[i];
|
|
@@ -3212,7 +3354,8 @@ async function start(apiKey, apiUrl) {
|
|
|
3212
3354
|
const barW = Math.min(20, startupTw - 40);
|
|
3213
3355
|
const filled = Math.round(pct * barW);
|
|
3214
3356
|
const bar = rgb(251, 191, 36) + '█'.repeat(filled) + rgb(50, 50, 70) + '░'.repeat(barW - filled) + c.reset;
|
|
3215
|
-
|
|
3357
|
+
balMoveToRow(balBaseRow);
|
|
3358
|
+
process.stdout.write(` ${rgb(251, 191, 36)}${spin}${c.reset} ${c.dim}Balance...${c.reset} ${bar} ${c.bold}${rgb(52, 211, 153)}${activeWorkers.length - balPending}${c.reset}${c.dim}/${c.reset}${c.white}${activeWorkers.length}${c.reset} \x1b[K`);
|
|
3216
3359
|
};
|
|
3217
3360
|
const balSpinnerInterval = setInterval(drawBalProgress, 80);
|
|
3218
3361
|
|
|
@@ -3228,7 +3371,9 @@ async function start(apiKey, apiUrl) {
|
|
|
3228
3371
|
const walletStr = `${c.green}⏣${wallet.toLocaleString()}${c.reset}`;
|
|
3229
3372
|
const bankStr = `${c.cyan}⏣${bank.toLocaleString()}${c.reset}`;
|
|
3230
3373
|
const totalStr = `${c.bold}⏣${(wallet + bank).toLocaleString()}${c.reset}`;
|
|
3231
|
-
|
|
3374
|
+
const row = balBaseRow + 1 + i;
|
|
3375
|
+
balMoveToRow(row);
|
|
3376
|
+
process.stdout.write(` ${num} ${rgb(52, 211, 153)}✓${c.reset} ${name} ${walletStr.padEnd(bColWallet + 4)} ${bankStr.padEnd(bColBank + 4)} ${totalStr.padEnd(bColTotal + 3)} ${lsColor}♥${ls}${c.reset}\x1b[K`);
|
|
3232
3377
|
balDone++;
|
|
3233
3378
|
}));
|
|
3234
3379
|
|