scripter-x 1.0.34 → 1.0.36

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "scripter-x",
3
- "version": "1.0.34",
3
+ "version": "1.0.36",
4
4
  "description": "ScripterX — local Flipkart session extractor (runs on your residential IP, syncs to marthunt)",
5
5
  "type": "module",
6
6
  "bin": {
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
- // INCREMENTAL SAVE: append every extracted session to one JSONL the instant it lands.
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 liveFile = join(baseDir, `${name}-${stamp}.jsonl`);
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
- appendFileSync(liveFile, JSON.stringify(res.session) + '\n');
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(` ${combined}`, 'accent');
701
- io.print(` (live JSONL: ${liveFile})`, 'accent');
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.
@@ -37,6 +37,9 @@ export function FullScreen({ children }) {
37
37
  // We register restore on exit + the kill signals so it runs no matter how we leave.
38
38
  export function enterAltScreen() {
39
39
  process.stdout.write('\x1b[?1049h'); // switch to alternate buffer
40
+ process.stdout.write('\x1b[r'); // reset scroll region to full screen (in case a prior
41
+ // session — e.g. an update-restart — left DECSTBM set,
42
+ // which would push our output to the bottom of the screen)
40
43
  process.stdout.write('\x1b[2J\x1b[H'); // clear + home
41
44
  process.stdout.write('\x1b[?25l'); // hide cursor (we draw our own ▏)
42
45
  let restored = false;
@@ -44,7 +47,10 @@ export function enterAltScreen() {
44
47
  if (restored) return;
45
48
  restored = true;
46
49
  try {
47
- process.stdout.write('\x1b[?1000l\x1b[?1003l\x1b[?1006l'); // disable ALL mouse modes
50
+ process.stdout.write('\x1b[?1000l\x1b[?1003l\x1b[?1006l\x1b[?1015l'); // disable ALL mouse modes
51
+ process.stdout.write('\x1b[?2004l'); // disable bracketed-paste
52
+ process.stdout.write('\x1b[r'); // reset scroll region (so typed text isn't stuck at the bottom)
53
+ process.stdout.write('\x1b[?7h'); // restore line wrap
48
54
  process.stdout.write('\x1b[?25h'); // show cursor
49
55
  process.stdout.write('\x1b[?1049l'); // back to normal buffer (history intact)
50
56
  } catch { /* */ }
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,13 +40,44 @@ 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
- try {
44
- const { stdout, stderr } = await execFileP('npm', ['install', '-g', `${PKG}@latest`], { timeout: 120000 });
45
- return { ok: true, output: (stdout + stderr).trim() };
46
- } catch (e) {
47
- return { ok: false, output: e.message };
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');
49
81
  }
50
82
 
51
83
  // Hard-reset the terminal so the freshly-spawned child inherits a CLEAN, cooked TTY.
@@ -63,10 +95,18 @@ export async function runUpdate() {
63
95
  function resetTerminal() {
64
96
  try {
65
97
  if (process.stdout.isTTY) {
66
- // disable button/motion/SGR mouse modes (the source of the coordinate-code garbage)
67
- process.stdout.write('\x1b[?1000l\x1b[?1003l\x1b[?1006l');
68
- process.stdout.write('\x1b[?25h'); // show cursor
69
- process.stdout.write('\x1b[?1049l'); // leave alt-screen → normal buffer
98
+ // Order matters. We must undo EVERY terminal mode the ink/fullscreen UI set, not just
99
+ // mouse + alt-screen — a leftover scroll region (DECSTBM) or bracketed-paste mode makes
100
+ // typed characters land at the bottom of the screen instead of at the prompt.
101
+ const w = (s) => process.stdout.write(s);
102
+ w('\x1b[?1000l\x1b[?1003l\x1b[?1006l\x1b[?1015l'); // disable ALL mouse modes
103
+ w('\x1b[?2004l'); // disable bracketed-paste mode
104
+ w('\x1b[r'); // reset scroll region (DECSTBM) to the full screen ← the bottom-text fix
105
+ w('\x1b[?7h'); // re-enable line wrap (autowrap)
106
+ w('\x1b[0m'); // reset SGR (colors/attrs)
107
+ w('\x1b[?1049l'); // leave alt-screen → normal buffer (history intact)
108
+ w('\x1b[?25h'); // show cursor
109
+ w('\x1b[!p'); // DECSTR: soft terminal reset (restores sane default modes)
70
110
  }
71
111
  } catch { /* */ }
72
112
  try {
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, 'extracted', true); // success: OTP consumed → no cancel
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}`);