dankgrinder 8.53.0 → 8.54.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.
Files changed (3) hide show
  1. package/lib/grinder.js +7 -7
  2. package/lib/ui.js +108 -95
  3. package/package.json +1 -1
package/lib/grinder.js CHANGED
@@ -3024,11 +3024,10 @@ async function start(apiKey, apiUrl, opts = {}) {
3024
3024
  const cpm = globalCmdRate.getRate().toFixed(1);
3025
3025
  console.log(`${c.bold}Total:${c.reset} +⏣${finalCoins.toLocaleString()} in ${formatUptime()} | ${finalCmds}cmds | ~${cpm}cmd/m | ${memFinal}MB`);
3026
3026
 
3027
- // Stop workers max 1s per worker so one hung client doesn't block shutdown
3028
- await Promise.all(workers.map(wk => Promise.race([
3029
- new Promise(resolve => { wk.stop(); resolve(true); }),
3030
- new Promise(resolve => setTimeout(() => resolve(false), 1000)),
3031
- ])));
3027
+ // Stop workers immediately (don't wait) instant shutdown
3028
+ for (const wk of workers) {
3029
+ try { wk.stop(); } catch {}
3030
+ }
3032
3031
  workerMap.clear();
3033
3032
 
3034
3033
  // Release cluster claims
@@ -3054,13 +3053,14 @@ async function start(apiKey, apiUrl, opts = {}) {
3054
3053
 
3055
3054
  if (redis) { redis.disconnect().catch(() => {}); }
3056
3055
  console.log(`${c.green}Goodbye!${c.reset}\n`);
3057
- // Force exit after 5s so Ctrl+C always terminates even if cleanup hangs
3058
- setTimeout(() => process.exit(0), 5000);
3056
+ // Force exit so Ctrl+C always terminates immediately
3057
+ setTimeout(() => process.exit(0), 2000);
3059
3058
  process.exit(0);
3060
3059
  }
3061
3060
 
3062
3061
  process.on('SIGINT', () => gracefulShutdown('SIGINT'));
3063
3062
  process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
3063
+ process.on('SIGHUP', () => gracefulShutdown('SIGHUP'));
3064
3064
  }
3065
3065
 
3066
3066
  // ══════════════════════════════════════════════════════════════
package/lib/ui.js CHANGED
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * CLI Live Dashboard — cursor-positioned in-place row updates.
3
- * Box drawn once at startup. Account rows updated in-place. Events below box.
3
+ * Box drawn once. Rows updated in-place. Events below box.
4
4
  */
5
5
 
6
6
  let _startTime = Date.now();
@@ -9,13 +9,15 @@ let _version = '0.0.0';
9
9
  let _live = false;
10
10
  let _phase = 'init';
11
11
 
12
- // Terminal dimensions at last draw
12
+ // Terminal dimensions
13
13
  let _W = 100;
14
14
  let _inner = 98;
15
15
  let _maxAccounts = 4;
16
16
 
17
- // Row map: which terminal row each account row starts at (1-indexed)
18
- let _accountRows = []; // _accountRows[accountIdx] = terminalRow
17
+ // Row map: which terminal row each account starts at (1-indexed)
18
+ let _accountRows = []; // _accountRows[accountIdx] = row
19
+ let _totalsRow = 0;
20
+ let _bottomRow = 0;
19
21
 
20
22
  // ── Spinner frames ────────────────────────────────────────────
21
23
  const SPINNER = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
@@ -50,20 +52,21 @@ const c = {
50
52
  white: '\x1b[37m',
51
53
  };
52
54
  const DIM = c.dim;
55
+ const B = '\x1b[38;2;77;212;238m'; // box border color
53
56
 
57
+ function row(n) { process.stdout.write(`\x1b[${n};1H`); }
58
+ function clrLine() { process.stdout.write('\x1b[2K'); }
54
59
  function trunc(s, n) { s = String(s || ''); return s.length <= n ? s : s.slice(0, n - 1) + '…'; }
55
60
  function padR(s, n) { return trunc(s, n).padEnd(n); }
56
61
  function padL(s, n, char) { return String(s).padStart(n, char || ' '); }
57
62
  function stripAnsi(s) { return String(s).replace(/\x1b\[[0-9;]*m/g, ''); }
58
- function clearLine() { process.stdout.write('\x1b[2K'); } // clear current line
63
+ function ln() { process.stdout.write('\n'); }
59
64
 
60
65
  function fmtUptime() {
61
66
  const s = Math.floor((Date.now() - _startTime) / 1000);
62
67
  if (s < 60) return `${s}s`;
63
68
  const m = Math.floor(s / 60);
64
69
  const h = Math.floor(m / 60);
65
- const d = Math.floor(h / 24);
66
- if (d > 0) return `${d}d${h % 24}h`;
67
70
  if (h > 0) return `${h}h${m % 60}m`;
68
71
  return `${m}m`;
69
72
  }
@@ -114,8 +117,24 @@ function fmtLevel(w) {
114
117
  return c.cyan + lv + c.reset;
115
118
  }
116
119
 
117
- // ── Build a full account row string ──────────────────────────
118
- function buildAccountRow(w, wi, col) {
120
+ // ── Layout ────────────────────────────────────────────────────
121
+ function layout() {
122
+ _W = Math.min(process.stdout.columns || 100, 120);
123
+ _inner = _W - 2;
124
+ const rows = process.stdout.rows || 40;
125
+ _maxAccounts = Math.min(_workers.length, Math.max(3, rows - 16));
126
+ }
127
+
128
+ // ── Column layout ─────────────────────────────────────────────
129
+ function getCol() {
130
+ const col = { st: 9, name: 18, cmd: 16, bal: 8, ls: 3, lv: 3, earned: 8 };
131
+ const nameExtra = Math.max(0, _inner - col.st - col.name - col.cmd - col.bal - col.ls - col.lv - col.earned - 14);
132
+ col.name += nameExtra;
133
+ return col;
134
+ }
135
+
136
+ // ── Build account row string ─────────────────────────────────
137
+ function accountRow(w, wi, col) {
119
138
  const col2 = wc(wi);
120
139
  const stCol = statusColor(w);
121
140
  const stTxt = statusText(w);
@@ -125,46 +144,54 @@ function buildAccountRow(w, wi, col) {
125
144
  const lv = fmtLevel(w);
126
145
  const name = padR(trunc(w.username || '?', col.name), col.name);
127
146
  const doing = padR(w.lastStatus || 'idle', col.cmd);
147
+ const earnedStr = stripAnsi(earned);
148
+ const balStr = stripAnsi(bal);
149
+
150
+ // Pad earned and bal to column widths
151
+ const earnedPadded = earned.padEnd(col.earned + (earned.length - earnedStr.length));
152
+ const balPadded = bal.padEnd(col.bal + (bal.length - balStr.length));
128
153
 
129
154
  return (
130
- '\x1b[2K' + // clear the line first
131
- `\x1b[38;2;77;212;238m│\x1b[0m ` +
155
+ `${B}│\x1b[0m ` +
132
156
  `${DIM}${padL(wi + 1, 2)}${c.reset} ` +
133
157
  `${stCol}${padR(stTxt, col.st)}${c.reset} ` +
134
158
  `${col2}${name}${c.reset} ` +
135
159
  `${DIM}${doing}${c.reset} ` +
136
- `${padL(bal, col.bal)} ` +
160
+ `${balPadded} ` +
137
161
  `${padL(ls, col.ls)} ` +
138
162
  `${padL(lv, col.lv)} ` +
139
- `${padL(earned, col.earned)} ` +
140
- `\x1b[38;2;77;212;238m│\x1b[0m\n`
163
+ `${earnedPadded} ` +
164
+ `${B}│\x1b[0m`
141
165
  );
142
166
  }
143
167
 
144
- // ── Draw the FULL box (called once at startup) ───────────────
168
+ // ── Draw the FULL box (called once) ─────────────────────────
145
169
  function draw() {
146
- const rows = process.stdout.rows || 40;
147
- _W = Math.min(process.stdout.columns || 100, 120);
148
- _inner = _W - 2;
170
+ layout();
171
+ const col = getCol();
172
+ const bannerH = BANNER_LINES.length;
149
173
 
150
- // Clear entire screen
174
+ // Clear entire screen + home
151
175
  process.stdout.write('\x1b[2J\x1b[H');
152
176
 
177
+ let r = 1;
178
+
153
179
  // ── Top border ──
154
- process.stdout.write(`\x1b[38;2;77;212;238m┌${'─'.repeat(_inner)}┐\x1b[0m\n`);
155
-
156
- // ── ASCII banner ──
157
- const bannerWithVer = BANNER_LINES[1].replace('PLACEHOLDER', _version);
158
- const bannerLines = [BANNER_LINES[0], bannerWithVer, ...BANNER_LINES.slice(2)];
159
- for (let i = 0; i < bannerLines.length; i++) {
160
- const line = bannerLines[i];
161
- const inner = _inner - stripAnsi(line).length;
180
+ row(r++); clrLine();
181
+ process.stdout.write(`${B}┌${'─'.repeat(_inner)}┐`);
182
+ ln();
183
+
184
+ // ── Banner ──
185
+ for (let i = 0; i < bannerH; i++) {
186
+ row(r++); clrLine();
187
+ const line = i === 1 ? BANNER_LINES[1].replace('PLACEHOLDER', _version) : BANNER_LINES[i];
162
188
  if (i < 2) {
163
189
  const gradLine = gradientLine(line, 77, 212, 238, 255, 92, 147);
164
- process.stdout.write(`\x1b[38;2;77;212;238m│\x1b[0m${gradLine}${' '.repeat(_inner - stripAnsi(gradLine).length)}\x1b[38;2;77;212;238m│\x1b[0m\n`);
190
+ process.stdout.write(`${B}│\x1b[0m ${gradLine}${' '.repeat(_inner - 2 - stripAnsi(gradLine) - 2)}${B}│`);
165
191
  } else {
166
- process.stdout.write(`\x1b[38;2;77;212;238m│\x1b[0m${DIM}${line}${c.reset}${' '.repeat(_inner - stripAnsi(line).length)}\x1b[38;2;77;212;238m│\x1b[0m\n`);
192
+ process.stdout.write(`${B}│\x1b[0m ${DIM}${line}${c.reset}${' '.repeat(_inner - 2 - stripAnsi(line) - 2)}${B}│`);
167
193
  }
194
+ ln();
168
195
  }
169
196
 
170
197
  // ── Status bar ──
@@ -179,34 +206,25 @@ function draw() {
179
206
  `${DIM}Ctrl+C${c.reset}`,
180
207
  ].filter(Boolean);
181
208
  const statusStr = statusParts.join(' ') + phaseLabel;
182
- process.stdout.write(`\x1b[38;2;77;212;238m│\x1b[0m ${statusStr}${' '.repeat(_inner - stripAnsi(statusStr))}\x1b[38;2;77;212;238m│\x1b[0m\n`);
183
- process.stdout.write(`\x1b[38;2;77;212;238m├${'─'.repeat(_inner)}┤\x1b[0m\n`);
184
209
 
185
- // ── Table header ──
186
- const col = { st: 9, name: 18, cmd: 16, bal: 8, ls: 3, lv: 3, earned: 8 };
187
- const nameExtra = Math.max(0, _inner - col.st - col.name - col.cmd - col.bal - col.ls - col.lv - col.earned - 14);
188
- col.name += nameExtra;
210
+ row(r++); clrLine();
211
+ process.stdout.write(`${B}│\x1b[0m ${statusStr}${' '.repeat(_inner - 1 - stripAnsi(statusStr))}${B}│`);
212
+ ln();
189
213
 
190
- process.stdout.write(`\x1b[2K\x1b[38;2;77;212;238m│\x1b[0m `);
191
- process.stdout.write(`${c.bold}#${c.reset} `);
192
- process.stdout.write(`${c.bold}${padR('STATUS', col.st)}${c.reset} `);
193
- process.stdout.write(`${c.bold}${padR('ACCOUNT', col.name)}${c.reset} `);
194
- process.stdout.write(`${c.bold}${padR('DOING', col.cmd)}${c.reset} `);
195
- process.stdout.write(`${c.bold}${padL('BAL', col.bal)}${c.reset} `);
196
- process.stdout.write(`${c.bold}${padL('LS', col.ls)}${c.reset} `);
197
- process.stdout.write(`${c.bold}${padL('LV', col.lv)}${c.reset} `);
198
- process.stdout.write(`${c.bold}${padL('EARNED', col.earned)}${c.reset} `);
199
- process.stdout.write(`\x1b[38;2;77;212;238m│\x1b[0m\n`);
200
- process.stdout.write(`\x1b[38;2;77;212;238m│\x1b[0m ${'─'.repeat(_inner)} \x1b[38;2;77;212;238m│\x1b[0m\n`);
201
-
202
- // Track row positions
203
- const BOX_TOP = 1;
204
- const bannerH = bannerLines.length; // 6
205
- const statusBarRow = BOX_TOP + bannerH + 1; // row 8
206
- const headerRow = statusBarRow + 1; // row 9
207
- const hrRow = headerRow + 1; // row 10
208
- _maxAccounts = Math.min(_workers.length, Math.max(3, rows - 14));
209
- _accountRows = [];
214
+ // ── Divider ──
215
+ row(r++); clrLine();
216
+ process.stdout.write(`${B}├${''.repeat(_inner)}┤`);
217
+ ln();
218
+
219
+ // ── Header ──
220
+ row(r++); clrLine();
221
+ process.stdout.write(`${B}│\x1b[0m ${c.bold}#${c.reset} ${c.bold}${padR('STATUS', col.st)}${c.reset} ${c.bold}${padR('ACCOUNT', col.name)}${c.reset} ${c.bold}${padR('DOING', col.cmd)}${c.reset} ${c.bold}${padL('BAL', col.bal)}${c.reset} ${c.bold}${padL('LS', col.ls)}${c.reset} ${c.bold}${padL('LV', col.lv)}${c.reset} ${c.bold}${padL('EARNED', col.earned)}${c.reset} ${B}│`);
222
+ ln();
223
+
224
+ // ── HR ──
225
+ row(r++); clrLine();
226
+ process.stdout.write(`${B}│\x1b[0m ${'─'.repeat(_inner)} ${B}│`);
227
+ ln();
210
228
 
211
229
  // ── Account rows ──
212
230
  const sorted = [..._workers].sort((a, b) => {
@@ -218,14 +236,22 @@ function draw() {
218
236
  });
219
237
  const shown = sorted.slice(0, _maxAccounts);
220
238
 
239
+ _accountRows = [];
221
240
  for (let si = 0; si < shown.length; si++) {
222
241
  const w = shown[si];
223
242
  const wi = _workers.indexOf(w);
224
- const rowNum = hrRow + 1 + si;
225
- _accountRows[wi] = rowNum;
226
- process.stdout.write(buildAccountRow(w, wi, col));
243
+ _accountRows[wi] = r;
244
+ row(r++); clrLine();
245
+ process.stdout.write(accountRow(w, wi, col));
246
+ ln();
227
247
  }
228
248
 
249
+ // ── Totals divider ──
250
+ _totalsRow = r;
251
+ row(r++); clrLine();
252
+ process.stdout.write(`${B}│\x1b[0m ${'─'.repeat(_inner)} ${B}│`);
253
+ ln();
254
+
229
255
  // ── Totals ──
230
256
  let totalCoins = 0, totalBal = 0;
231
257
  for (const w of _workers) {
@@ -233,40 +259,35 @@ function draw() {
233
259
  totalBal += w.stats.balance || 0;
234
260
  }
235
261
  const memMB = Math.round((process.memoryUsage?.rss?.() ?? process.memoryUsage().rss) / 1048576);
262
+ const balStr = totalBal > 0 ? '⏣' + totalBal.toLocaleString() : '—';
263
+ const balPadded = balStr.padEnd(col.bal);
264
+ const earnedStr = stripAnsi(fmtCoins(totalCoins));
236
265
 
237
- const totalsRow = hrRow + 1 + shown.length;
238
- process.stdout.write(`\x1b[38;2;77;212;238m│\x1b[0m ${'─'.repeat(_inner)} \x1b[38;2;77;212;238m│\x1b[0m\n`);
239
- process.stdout.write(`\x1b[2K\x1b[38;2;77;212;238m│\x1b[0m `);
240
- process.stdout.write(`${c.bold}Σ${c.reset} `);
241
- process.stdout.write(`${DIM}${padL(_workers.length, 2)} acc${c.reset} `);
242
- process.stdout.write(`${' '.repeat(col.name)} `);
243
- process.stdout.write(`${' '.repeat(col.cmd)} `);
244
- process.stdout.write(`${padL(totalBal > 0 ? '⏣' + totalBal.toLocaleString() : '—', col.bal)} `);
245
- process.stdout.write(`${' '.repeat(col.ls)} `);
246
- process.stdout.write(`${' '.repeat(col.lv)} `);
247
- process.stdout.write(`${fmtCoins(totalCoins)} `);
248
- process.stdout.write(`${DIM}${fmtUptime()} | ${memMB}MB${c.reset} `.padEnd(_inner));
249
- process.stdout.write(`\x1b[38;2;77;212;238m│\x1b[0m\n`);
266
+ row(r++); clrLine();
267
+ process.stdout.write(
268
+ `${B}│\x1b[0m ${c.bold}Σ${c.reset} ${DIM}${padL(_workers.length, 2)} acc${c.reset} ${' '.repeat(col.name + col.cmd)} ${balPadded} ${' '.repeat(col.ls + col.lv)} ${fmtCoins(totalCoins)}${' '.repeat(_inner - 2 - stripAnsi(` ${c.bold}Σ${c.reset} ${DIM}${padL(_workers.length, 2)} acc${c.reset} ${' '.repeat(col.name + col.cmd)} ${balPadded} ${' '.repeat(col.ls + col.lv)} ${fmtCoins(totalCoins)}`))}${B}│`
269
+ );
270
+ ln();
250
271
 
251
272
  // ── Bottom ──
252
- process.stdout.write(`\x1b[38;2;77;212;238m└${'─'.repeat(_inner)}┘\x1b[0m\n`);
273
+ _bottomRow = r;
274
+ row(r++); clrLine();
275
+ process.stdout.write(`${B}└${'─'.repeat(_inner)}┘`);
276
+ ln();
253
277
  }
254
278
 
255
- // ── Update ONE account row in place ─────────────────────────
279
+ // ── Update ONE account row in-place ─────────────────────────
256
280
  function updateAccountRow(accountIdx) {
257
- const row = _accountRows[accountIdx];
258
- if (!row) return; // not visible
259
-
281
+ if (!_live) return;
282
+ const rowNum = _accountRows[accountIdx];
283
+ if (!rowNum) return; // not visible
260
284
  const w = _workers[accountIdx];
261
285
  if (!w) return;
286
+ const col = getCol();
262
287
 
263
- const col = { st: 9, name: 18, cmd: 16, bal: 8, ls: 3, lv: 3, earned: 8 };
264
- const nameExtra = Math.max(0, _inner - col.st - col.name - col.cmd - col.bal - col.ls - col.lv - col.earned - 14);
265
- col.name += nameExtra;
266
-
267
- // Move cursor to this row, column 1
268
- process.stdout.write(`\x1b[${row};1H`);
269
- process.stdout.write(buildAccountRow(w, accountIdx, col));
288
+ row(rowNum); clrLine();
289
+ process.stdout.write(accountRow(w, accountIdx, col));
290
+ ln();
270
291
  }
271
292
 
272
293
  // ── Gradient line ─────────────────────────────────────────────
@@ -280,7 +301,7 @@ function gradientLine(text, r1, g1, b1, r2, g2, b2) {
280
301
  }
281
302
 
282
303
  // ── Event tracking ────────────────────────────────────────────
283
- let _eventLines = []; // [accountIdx] = [{text, ts}]
304
+ let _eventLines = [];
284
305
  const MAX_EVENTS = 15;
285
306
 
286
307
  // ── Public API ────────────────────────────────────────────────
@@ -307,7 +328,6 @@ function stop() {
307
328
  function setLive(val) { _live = val; }
308
329
  function setPhase(phase) { _phase = phase; }
309
330
 
310
- // log: update account row in-place + append event below box
311
331
  function log(accountIdx, msg) {
312
332
  const now = new Date();
313
333
  const ts = `${padL(now.getHours(), 2, '0')}:${padL(now.getMinutes(), 2, '0')}:${padL(now.getSeconds(), 2, '0')}`;
@@ -316,13 +336,11 @@ function log(accountIdx, msg) {
316
336
  if (!_eventLines[accountIdx]) _eventLines[accountIdx] = [];
317
337
  _eventLines[accountIdx].push({ text: msg, ts });
318
338
  if (_eventLines[accountIdx].length > MAX_EVENTS) _eventLines[accountIdx].shift();
319
- // Update the row in place
320
339
  if (_live) updateAccountRow(accountIdx);
321
340
  }
322
341
 
323
342
  if (!_live) return;
324
343
 
325
- // Append event below the box (after the bottom border)
326
344
  const col2 = accountIdx >= 0 ? wc(accountIdx) : c.cyan;
327
345
  const name = accountIdx >= 0 ? trunc(_workers[accountIdx]?.username || '?', 14) : 'GLOBAL';
328
346
  process.stdout.write(`${col2}${name}${c.reset} ${DIM}[${ts}]${c.reset} ${msg}\n`);
@@ -330,22 +348,17 @@ function log(accountIdx, msg) {
330
348
 
331
349
  function logGlobal(msg) { log(-1, msg); }
332
350
 
333
- // Force full redraw (call this when window resizes or box needs rebuild)
334
- function fullRedraw() { if (_live) draw(); }
335
-
336
- // ── Periodic refresh: animate spinners & countdowns every 3s ──
351
+ // ── Refresh ──────────────────────────────────────────────────
337
352
  let _refreshTimer = null;
338
353
  function startRefresh() {
339
354
  if (_refreshTimer) return;
340
355
  _refreshTimer = setInterval(() => {
341
356
  if (!_live) return;
342
- for (let i = 0; i < _workers.length; i++) {
343
- updateAccountRow(i);
344
- }
357
+ for (let i = 0; i < _workers.length; i++) updateAccountRow(i);
345
358
  }, 1500);
346
359
  }
347
360
  function stopRefresh() {
348
361
  if (_refreshTimer) { clearInterval(_refreshTimer); _refreshTimer = null; }
349
362
  }
350
363
 
351
- module.exports = { init, drawBanner, start, draw, log, logGlobal, workerColor: wc, stop, setLive, setPhase, updateAccountRow, fullRedraw, startRefresh, stopRefresh };
364
+ module.exports = { init, drawBanner, start, draw, log, logGlobal, workerColor: wc, stop, setLive, setPhase, updateAccountRow, startRefresh, stopRefresh };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dankgrinder",
3
- "version": "8.53.0",
3
+ "version": "8.54.0",
4
4
  "description": "Dank Memer automation engine — grind coins while you sleep",
5
5
  "bin": {
6
6
  "dankgrinder": "bin/dankgrinder.js"