scripter-x 1.0.33 → 1.0.35
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/package.json +1 -1
- package/src/commands.js +26 -10
- package/src/index.js +1 -0
- package/src/providers/bigbasket.js +22 -0
- package/src/update.js +88 -14
- package/src/worker.js +14 -2
package/package.json
CHANGED
package/src/commands.js
CHANGED
|
@@ -653,17 +653,32 @@ async function bigbasketAuto(io, args = {}) {
|
|
|
653
653
|
const { RunController } = await import('./controller.js');
|
|
654
654
|
const controller = new RunController({ name, provider: `tempotp-bigbasket`, requested: count });
|
|
655
655
|
|
|
656
|
-
//
|
|
656
|
+
// Optionally check My Orders per account → split NEW (0 orders) vs OLD (≥1 order).
|
|
657
|
+
const checkOrders = args.checkOrders != null ? args.checkOrders
|
|
658
|
+
: await io.confirm('check My Orders + split into new / old account files?', true);
|
|
659
|
+
|
|
660
|
+
// INCREMENTAL SAVE to a single .json array — rewritten on every success so a mid-run
|
|
661
|
+
// stop / crash never loses an account, and there's exactly ONE file (no .jsonl).
|
|
662
|
+
// With --check-orders, successes are also written to per-bucket new/old files.
|
|
657
663
|
const baseDir = bigbasketBaseDir(args);
|
|
658
664
|
const stamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
659
|
-
const
|
|
665
|
+
const mainFile = join(baseDir, `${name}-${stamp}.json`);
|
|
666
|
+
const newFile = join(baseDir, `${name}-${stamp}-new.json`);
|
|
667
|
+
const oldFile = join(baseDir, `${name}-${stamp}-old.json`);
|
|
668
|
+
const collected = [];
|
|
660
669
|
const onResult = (res) => {
|
|
661
670
|
if (res.status !== 'success' || !res.session) return;
|
|
662
671
|
mkdirSync(baseDir, { recursive: true });
|
|
663
|
-
|
|
672
|
+
collected.push(res.session);
|
|
673
|
+
writeFileSync(mainFile, JSON.stringify(collected, null, 2) + '\n');
|
|
674
|
+
if (checkOrders) {
|
|
675
|
+
const dest = res.session.is_new ? newFile : oldFile;
|
|
676
|
+
const bucket = collected.filter((s) => (s.is_new === true) === (res.session.is_new === true));
|
|
677
|
+
writeFileSync(dest, JSON.stringify(bucket, null, 2) + '\n');
|
|
678
|
+
}
|
|
664
679
|
};
|
|
665
680
|
|
|
666
|
-
const worker = new BigBasketWorker(provider, { requested: count, concurrency, onEvent: controller.handleEvent, onResult });
|
|
681
|
+
const worker = new BigBasketWorker(provider, { requested: count, concurrency, checkOrders, onEvent: controller.handleEvent, onResult });
|
|
667
682
|
controller.stop = () => worker.stop();
|
|
668
683
|
|
|
669
684
|
const results = [];
|
|
@@ -690,15 +705,16 @@ async function bigbasketAuto(io, args = {}) {
|
|
|
690
705
|
|
|
691
706
|
io.print(` ✓ done — ${worker.stats.succeeded} succeeded · ${worker.stats.failed} failed · ${worker.stats.cancelled} cancelled · ₹${worker.stats.charges} spent`);
|
|
692
707
|
|
|
693
|
-
// Also write a combined pretty JSON array of all successes for convenience.
|
|
694
708
|
const succeeded = results.filter((r) => r.status === 'success' && r.session).map((r) => r.session);
|
|
695
709
|
if (succeeded.length) {
|
|
696
|
-
mkdirSync(baseDir, { recursive: true });
|
|
697
|
-
const combined = join(baseDir, `${name}-${stamp}.json`);
|
|
698
|
-
writeFileSync(combined, JSON.stringify(succeeded, null, 2) + '\n');
|
|
699
710
|
io.print(` ✓ saved ${succeeded.length} session(s):`);
|
|
700
|
-
io.print(` ${
|
|
701
|
-
|
|
711
|
+
io.print(` ${mainFile}`, 'accent');
|
|
712
|
+
if (checkOrders) {
|
|
713
|
+
const nNew = succeeded.filter((s) => s.is_new).length;
|
|
714
|
+
const nOld = succeeded.length - nNew;
|
|
715
|
+
io.print(` ${nNew} new → ${newFile}`, 'accent');
|
|
716
|
+
io.print(` ${nOld} old → ${oldFile}`, 'accent');
|
|
717
|
+
}
|
|
702
718
|
} else {
|
|
703
719
|
io.print(' ◉ no sessions extracted — nothing to save.');
|
|
704
720
|
}
|
package/src/index.js
CHANGED
|
@@ -124,6 +124,7 @@ async function main() {
|
|
|
124
124
|
target: flags.target, // run: 'flipkart' | 'zepto'
|
|
125
125
|
number: flags.number, otp: flags.otp, cert: flags.cert, // zepto: local OTP login → ZAUTH1 envelope
|
|
126
126
|
service: flags.service, // bigbasket auto: TempOTP service id (1819/1874/1919/2259)
|
|
127
|
+
checkOrders: flags['check-orders'] === true ? true : (flags['check-orders'] === false ? false : null), // bigbasket: split new/old by My Orders
|
|
127
128
|
auto: flags.auto, manual: flags.manual,
|
|
128
129
|
checkCoupon: flags['check-coupon'] === true ? true : (flags['check-coupon'] === false ? false : null),
|
|
129
130
|
campaign: flags._[0], out: flags.out, action: flags._[0], value: flags._[1],
|
|
@@ -212,6 +212,28 @@ export async function verifyOtp(session, otp) {
|
|
|
212
212
|
return { ok: false, status: r.status, code, error };
|
|
213
213
|
}
|
|
214
214
|
|
|
215
|
+
// Fetch the account's order count to classify NEW (0 orders) vs OLD (≥1).
|
|
216
|
+
// Primary signal is member/details.active_order_count (confirmed 200 on the native API).
|
|
217
|
+
// NOTE: active_order_count counts in-progress orders; a returning customer with only
|
|
218
|
+
// delivered orders can read 0 here. For freshly-extracted accounts the strongest signal is
|
|
219
|
+
// the verify-time is_signup flag (a brand-new account == NEW); this call refines it. Returns
|
|
220
|
+
// { count, isNew, name, email } and never throws.
|
|
221
|
+
export async function getOrderCount(session) {
|
|
222
|
+
try {
|
|
223
|
+
if (session.token) session.jar.BBAUTHTOKEN = session.token;
|
|
224
|
+
await throttle.wait();
|
|
225
|
+
const r = await req(session, 'GET', '/ui-svc/v1/member/details');
|
|
226
|
+
const md = r.json?.member_details || {};
|
|
227
|
+
const count = Number(md.active_order_count ?? 0);
|
|
228
|
+
// hasOrdered: any past order at all. active_order_count>0 is a definite yes; otherwise
|
|
229
|
+
// fall back to the signup flag (a freshly-created account has never ordered).
|
|
230
|
+
const hasOrdered = count > 0 || (session.isSignup === false && !!md.email);
|
|
231
|
+
return { ok: r.status === 200, count, isNew: !hasOrdered, name: md.first_name || '', email: md.email || '' };
|
|
232
|
+
} catch {
|
|
233
|
+
return { ok: false, count: 0, isNew: session.isSignup !== false, name: '', email: '' };
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
215
237
|
// Step 3 (new users only) — set the name to activate the freshly-created account.
|
|
216
238
|
// POST /member-tdl/v3/member/profile/ with the BBAUTHTOKEN we just got. Returns true on
|
|
217
239
|
// success. Never throws — a name-set failure shouldn't discard the token.
|
package/src/update.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import { execFile, spawn } from 'node:child_process';
|
|
7
7
|
import { promisify } from 'node:util';
|
|
8
8
|
import process from 'node:process';
|
|
9
|
+
import { dirname, join } from 'node:path';
|
|
9
10
|
import * as config from './config.js';
|
|
10
11
|
|
|
11
12
|
const execFileP = promisify(execFile);
|
|
@@ -39,53 +40,126 @@ export async function checkForUpdate(current) {
|
|
|
39
40
|
}
|
|
40
41
|
|
|
41
42
|
// Run the global update. Returns { ok, output }.
|
|
43
|
+
//
|
|
44
|
+
// Robustness: a bare execFile('npm', …) frequently fails with "Command failed" because the
|
|
45
|
+
// spawned process doesn't inherit the user's interactive PATH (npm/node installed via nvm,
|
|
46
|
+
// homebrew, fnm, etc. live in dirs the login shell adds but a non-login spawn doesn't). We
|
|
47
|
+
// run through the user's shell so `npm` resolves the same way it does in their terminal, and
|
|
48
|
+
// fall back across a couple of resolutions. On failure we return the REAL stderr so the user
|
|
49
|
+
// sees why (EACCES, ENOTFOUND, etc.) instead of a generic "Command failed".
|
|
42
50
|
export async function runUpdate() {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
51
|
+
const args = ['install', '-g', `${PKG}@latest`];
|
|
52
|
+
// 1) try a login+interactive shell so PATH matches the user's terminal exactly.
|
|
53
|
+
const shell = process.env.SHELL || '/bin/sh';
|
|
54
|
+
const attempts = [
|
|
55
|
+
() => execFileP(shell, ['-lic', `npm ${args.join(' ')}`], { timeout: 180000 }),
|
|
56
|
+
() => execFileP('npm', args, { timeout: 180000 }), // npm already on PATH
|
|
57
|
+
() => execFileP(process.execPath, [npmCliPath(), ...args], { timeout: 180000 }), // node + npm-cli.js
|
|
58
|
+
];
|
|
59
|
+
let lastErr = '';
|
|
60
|
+
for (const run of attempts) {
|
|
61
|
+
try {
|
|
62
|
+
const { stdout, stderr } = await run();
|
|
63
|
+
return { ok: true, output: (stdout + stderr).trim() };
|
|
64
|
+
} catch (e) {
|
|
65
|
+
lastErr = (e.stderr || e.stdout || e.message || '').toString().trim();
|
|
66
|
+
// permission error → tell the user to use sudo (don't auto-sudo; that needs a TTY prompt)
|
|
67
|
+
if (/EACCES|permission denied/i.test(lastErr)) {
|
|
68
|
+
return { ok: false, output: `permission denied — run: sudo npm install -g ${PKG}@latest` };
|
|
69
|
+
}
|
|
70
|
+
}
|
|
48
71
|
}
|
|
72
|
+
return { ok: false, output: lastErr || 'could not run npm' };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Best-effort path to npm's CLI entry (npm-cli.js) next to the running node binary, so we can
|
|
76
|
+
// invoke it directly when `npm` isn't resolvable as a command.
|
|
77
|
+
function npmCliPath() {
|
|
78
|
+
// node lives in <prefix>/bin/node; npm-cli.js is at <prefix>/lib/node_modules/npm/bin/npm-cli.js
|
|
79
|
+
const dir = dirname(process.execPath);
|
|
80
|
+
return join(dir, '..', 'lib', 'node_modules', 'npm', 'bin', 'npm-cli.js');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Hard-reset the terminal so the freshly-spawned child inherits a CLEAN, cooked TTY.
|
|
84
|
+
//
|
|
85
|
+
// THE BUG this fixes: after the old ink app unmounts, the terminal could still be left in
|
|
86
|
+
// raw mode WITH SGR mouse tracking enabled (`\x1b[?1000h…`). When the restarted process
|
|
87
|
+
// took over that same TTY, every keypress/click emitted raw escape/coordinate codes that
|
|
88
|
+
// got echoed as garbage ("random shit on every keypress") until the user killed + relaunched
|
|
89
|
+
// manually. ink's alt-screen restore only ran on `process.on('exit')` AND the old + new
|
|
90
|
+
// processes briefly shared the TTY, so the disable sequences raced the child's re-enable.
|
|
91
|
+
//
|
|
92
|
+
// Fix: synchronously (1) disable ALL mouse modes + restore the screen, (2) take stdin out of
|
|
93
|
+
// raw mode, pause it, and drop every listener — BEFORE the child is spawned — so there is no
|
|
94
|
+
// window where mouse reporting or a stray raw-mode listener can corrupt the child's input.
|
|
95
|
+
function resetTerminal() {
|
|
96
|
+
try {
|
|
97
|
+
if (process.stdout.isTTY) {
|
|
98
|
+
// disable button/motion/SGR mouse modes (the source of the coordinate-code garbage)
|
|
99
|
+
process.stdout.write('\x1b[?1000l\x1b[?1003l\x1b[?1006l');
|
|
100
|
+
process.stdout.write('\x1b[?25h'); // show cursor
|
|
101
|
+
process.stdout.write('\x1b[?1049l'); // leave alt-screen → normal buffer
|
|
102
|
+
}
|
|
103
|
+
} catch { /* */ }
|
|
104
|
+
try {
|
|
105
|
+
if (process.stdin.isTTY && typeof process.stdin.setRawMode === 'function') {
|
|
106
|
+
process.stdin.setRawMode(false); // back to cooked/canonical mode
|
|
107
|
+
}
|
|
108
|
+
// drop every data/keypress listener the old ink app (or our mouse hook) attached, then
|
|
109
|
+
// pause so nothing in THIS process consumes input meant for the child.
|
|
110
|
+
process.stdin.removeAllListeners('data');
|
|
111
|
+
process.stdin.removeAllListeners('keypress');
|
|
112
|
+
process.stdin.pause();
|
|
113
|
+
} catch { /* */ }
|
|
49
114
|
}
|
|
50
115
|
|
|
51
116
|
// Re-exec the CLI so the just-installed version takes over. Spawns a fresh, DETACHED
|
|
52
117
|
// process that inherits this terminal, then exits the current one. The caller MUST have
|
|
53
|
-
// already torn down the ink/alt-screen UI
|
|
54
|
-
// this — otherwise the child renders over a dirty screen.
|
|
118
|
+
// already torn down the ink/alt-screen UI before calling this.
|
|
55
119
|
//
|
|
56
120
|
// We re-run the CURRENT entry file with node (`process.argv[1]`): after `npm install -g`,
|
|
57
121
|
// the global bin symlink points at the new version, so this loads the updated code. If
|
|
58
122
|
// that entry path is somehow missing, we fall back to the `scripterx` bin on PATH.
|
|
59
123
|
//
|
|
124
|
+
// ORDERING is what makes the restart clean: we resetTerminal() FIRST (cooked TTY, mouse off,
|
|
125
|
+
// stdin drained), THEN spawn the child, THEN exit immediately on the next tick. The child
|
|
126
|
+
// only sets up raw mode / mouse tracking after it boots — by which point the parent is gone,
|
|
127
|
+
// so the two never fight over the TTY. No more leaked keystrokes.
|
|
128
|
+
//
|
|
60
129
|
// NOTE: on success it exits the process and never returns. Returns false only if BOTH
|
|
61
130
|
// spawn attempts throw synchronously, so the caller can show "restart manually".
|
|
62
|
-
export function restartCli({ binName = 'scripterx'
|
|
131
|
+
export function restartCli({ binName = 'scripterx' } = {}) {
|
|
63
132
|
// Re-run with the SAME args the user launched with (drop node + the script path), so
|
|
64
133
|
// `scripterx` → interactive shell, `scripterx zepto` → zepto, etc.
|
|
65
134
|
const argv = process.argv.slice(2);
|
|
66
135
|
const entry = process.argv[1];
|
|
67
136
|
|
|
137
|
+
// Clean the terminal BEFORE spawning so the child never inherits raw mode / mouse mode.
|
|
138
|
+
resetTerminal();
|
|
139
|
+
|
|
68
140
|
const trySpawn = (cmd, args) => {
|
|
69
141
|
const child = spawn(cmd, args, { stdio: 'inherit', detached: true, shell: false });
|
|
70
142
|
child.unref();
|
|
71
143
|
return child;
|
|
72
144
|
};
|
|
73
145
|
|
|
146
|
+
const exitSoon = () => {
|
|
147
|
+
// Exit on the next tick (not after a long timer) so we don't linger on the TTY. The
|
|
148
|
+
// detached child has already inherited the (now clean) fds and takes over.
|
|
149
|
+
setImmediate(() => process.exit(0));
|
|
150
|
+
};
|
|
151
|
+
|
|
74
152
|
try {
|
|
75
153
|
const primary = trySpawn(process.execPath, [entry, ...argv]);
|
|
76
|
-
// If the entry re-exec fails to even start, fall back to the named bin on PATH.
|
|
77
154
|
primary.on('error', () => {
|
|
78
155
|
try { trySpawn(binName, argv); } catch { /* nothing more we can do */ }
|
|
79
156
|
});
|
|
80
|
-
|
|
81
|
-
// detached child takes over the same TTY running the new version.
|
|
82
|
-
setTimeout(() => process.exit(0), delayMs);
|
|
157
|
+
exitSoon();
|
|
83
158
|
return true;
|
|
84
159
|
} catch {
|
|
85
|
-
// process.execPath spawn threw synchronously — try the named bin directly.
|
|
86
160
|
try {
|
|
87
161
|
trySpawn(binName, argv);
|
|
88
|
-
|
|
162
|
+
exitSoon();
|
|
89
163
|
return true;
|
|
90
164
|
} catch {
|
|
91
165
|
return false;
|
package/src/worker.js
CHANGED
|
@@ -773,11 +773,12 @@ export class ZeptoWorker {
|
|
|
773
773
|
// rentOnce/startOtp(poll)/cancel. Local-first: emits res.session for incremental save;
|
|
774
774
|
// server push is layered on top by the caller (onResult) when a backend is configured.
|
|
775
775
|
export class BigBasketWorker {
|
|
776
|
-
constructor(provider, { requested, concurrency, deadlineMs, onEvent, onResult }) {
|
|
776
|
+
constructor(provider, { requested, concurrency, deadlineMs, checkOrders, onEvent, onResult }) {
|
|
777
777
|
this.provider = provider;
|
|
778
778
|
this.requested = requested;
|
|
779
779
|
this.concurrency = Math.max(1, Math.min(concurrency, requested));
|
|
780
780
|
this.deadlineMs = deadlineMs || (provider?.name === 'tempotp' ? TEMPOTP_DEADLINE : OTPCART_DEADLINE);
|
|
781
|
+
this.checkOrders = !!checkOrders; // classify NEW (0 orders) vs OLD (≥1) per account
|
|
781
782
|
this.onEvent = onEvent || (() => {});
|
|
782
783
|
this.onResult = onResult || null;
|
|
783
784
|
if (provider && 'onNotice' in provider) provider.onNotice = (msg) => this.onEvent('warn', `⚠ ${msg}`);
|
|
@@ -951,9 +952,20 @@ export class BigBasketWorker {
|
|
|
951
952
|
res.cost = number.cost;
|
|
952
953
|
res.session = bigbasket.toSessionKeys(session); // { phone, bbAuthToken, mId, bbVisitorId }
|
|
953
954
|
res.category = 'extracted';
|
|
955
|
+
// is_new defaults to the signup signal (brand-new account == NEW). Refine with the
|
|
956
|
+
// order check when enabled.
|
|
957
|
+
res.session.is_new = session.isSignup !== false;
|
|
958
|
+
|
|
959
|
+
if (this.checkOrders) {
|
|
960
|
+
this._emit(slot, { phase: 'minutes', detail: 'checking My Orders' });
|
|
961
|
+
const oc = await bigbasket.getOrderCount(session);
|
|
962
|
+
res.session.orderCount = oc.count;
|
|
963
|
+
res.session.is_new = oc.isNew;
|
|
964
|
+
res.category = oc.isNew ? 'fresh' : 'extracted';
|
|
965
|
+
}
|
|
954
966
|
|
|
955
967
|
this._emit(slot, { phase: 'saving', detail: 'saving session' });
|
|
956
|
-
releaseOnce(true, '
|
|
968
|
+
releaseOnce(true, res.session.is_new ? 'new account' : 'old account', true); // OTP consumed → no cancel
|
|
957
969
|
return res;
|
|
958
970
|
} catch (e) {
|
|
959
971
|
return fail(`unexpected: ${e.message}`);
|