scripter-x 1.0.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/README.md +55 -0
- package/package.json +33 -0
- package/src/api.js +89 -0
- package/src/commands.js +247 -0
- package/src/config.js +67 -0
- package/src/controller.js +27 -0
- package/src/flipkart.js +186 -0
- package/src/index.js +105 -0
- package/src/prompt.js +60 -0
- package/src/providers/otpcart.js +95 -0
- package/src/providers/tempotp.js +83 -0
- package/src/theme.js +59 -0
- package/src/throttle.js +23 -0
- package/src/ui/App.js +101 -0
- package/src/ui/RunView.js +91 -0
- package/src/ui/Shell.js +114 -0
- package/src/ui/components.js +84 -0
- package/src/ui/mouse.js +95 -0
- package/src/util.js +54 -0
- package/src/worker.js +261 -0
package/README.md
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# scripter-x
|
|
2
|
+
|
|
3
|
+
Local Flipkart session extractor. It rents virtual numbers, logs into Flipkart, and
|
|
4
|
+
extracts the login session JSON — **running on your own machine (your residential IP)**,
|
|
5
|
+
which is the only way the Flipkart login-verify completes (datacenter IPs get blocked).
|
|
6
|
+
Results sync automatically to your ScripterX dashboard for storage + export.
|
|
7
|
+
|
|
8
|
+
## Install
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
npm install -g scripter-x
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
That installs the `scripterx` command. Needs Node 18+.
|
|
15
|
+
|
|
16
|
+
## Use
|
|
17
|
+
|
|
18
|
+
Just run it for the interactive shell:
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
scripterx
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
You get a prompt. Type a command, or press **`/`** to open the command palette.
|
|
25
|
+
|
|
26
|
+
### Selecting from a menu — three ways
|
|
27
|
+
- **Press `1`–`9`** to instantly pick that row ⭐ (most reliable)
|
|
28
|
+
- **`↑`/`↓` then Enter** to navigate
|
|
29
|
+
- **Click** a row (best-effort; works when the screen hasn't scrolled)
|
|
30
|
+
|
|
31
|
+
Yes/no questions: **`←`/`→`** to toggle (or `y`/`n`), Enter to accept.
|
|
32
|
+
|
|
33
|
+
### One-shot commands
|
|
34
|
+
You can also run any command directly without the shell:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
scripterx login
|
|
38
|
+
scripterx run --provider otpcart --count 10 --concurrency 2 --check-minutes
|
|
39
|
+
scripterx campaigns
|
|
40
|
+
scripterx export <name> # → ~/Downloads/scripterx/<name>.json
|
|
41
|
+
scripterx balance
|
|
42
|
+
scripterx config set server <url>
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## How it works
|
|
46
|
+
1. **You** run it locally → all Flipkart calls go from your residential IP (with a
|
|
47
|
+
built-in 2-second gap between calls so you're never rate-limited).
|
|
48
|
+
2. It rents a number (OTPCart or TempOTP), sends the OTP, reads the SMS, verifies,
|
|
49
|
+
and extracts the full session.
|
|
50
|
+
3. Each result is pushed to your ScripterX server (encrypted at rest) — view and export
|
|
51
|
+
from the web dashboard, or auto-saved locally to `~/Downloads/scripterx/`.
|
|
52
|
+
4. **Only successful extractions cost money** — failed/cancelled numbers are refunded.
|
|
53
|
+
|
|
54
|
+
Config + token live in `~/.scripterx/config.json`. Point at a different backend with
|
|
55
|
+
`scripterx config set server <url>` or the `SCRIPTERX_SERVER` env var.
|
package/package.json
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "scripter-x",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "ScripterX — local Flipkart session extractor (runs on your residential IP, syncs to marthunt)",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"scripterx": "src/index.js"
|
|
8
|
+
},
|
|
9
|
+
"engines": {
|
|
10
|
+
"node": ">=18"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"src",
|
|
14
|
+
"README.md"
|
|
15
|
+
],
|
|
16
|
+
"keywords": [
|
|
17
|
+
"flipkart",
|
|
18
|
+
"cli",
|
|
19
|
+
"scripterx",
|
|
20
|
+
"session",
|
|
21
|
+
"otp"
|
|
22
|
+
],
|
|
23
|
+
"scripts": {
|
|
24
|
+
"start": "node src/index.js"
|
|
25
|
+
},
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"ink": "^5.0.1",
|
|
28
|
+
"ink-spinner": "^5.0.0",
|
|
29
|
+
"react": "^18.3.1",
|
|
30
|
+
"ws": "^8.18.0"
|
|
31
|
+
},
|
|
32
|
+
"license": "MIT"
|
|
33
|
+
}
|
package/src/api.js
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// Client for the marthunt ScripterX backend (auth + campaign + ingest + export).
|
|
2
|
+
// Uses Node's built-in fetch (Node 18+). The CLI never touches the DB directly.
|
|
3
|
+
import * as config from './config.js';
|
|
4
|
+
|
|
5
|
+
export class AuthError extends Error {}
|
|
6
|
+
export class ApiError extends Error {}
|
|
7
|
+
|
|
8
|
+
export class ApiClient {
|
|
9
|
+
constructor({ baseUrl, token } = {}) {
|
|
10
|
+
const cfg = config.load();
|
|
11
|
+
this.base = (baseUrl || cfg.server_base_url).replace(/\/+$/, '');
|
|
12
|
+
this.token = token || cfg.jwt || null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async _req(method, path, { auth = true, body } = {}) {
|
|
16
|
+
const headers = { 'Content-Type': 'application/json', Accept: 'application/json' };
|
|
17
|
+
if (auth && this.token) headers.Authorization = `Bearer ${this.token}`;
|
|
18
|
+
let res;
|
|
19
|
+
try {
|
|
20
|
+
res = await fetch(this.base + path, {
|
|
21
|
+
method, headers, body: body ? JSON.stringify(body) : undefined,
|
|
22
|
+
});
|
|
23
|
+
} catch (e) {
|
|
24
|
+
throw new ApiError(`cannot reach server (${this.base}): ${e.message}`);
|
|
25
|
+
}
|
|
26
|
+
if (res.status === 401) throw new AuthError('session expired — run `scripterx login`');
|
|
27
|
+
let data;
|
|
28
|
+
try { data = await res.json(); } catch {
|
|
29
|
+
throw new ApiError(`server returned ${res.status}`);
|
|
30
|
+
}
|
|
31
|
+
if (!res.ok || data.success === false) {
|
|
32
|
+
throw new ApiError(data.error || `server error ${res.status}`);
|
|
33
|
+
}
|
|
34
|
+
return data;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// ── auth ──
|
|
38
|
+
async login(username, password) {
|
|
39
|
+
const d = await this._req('POST', '/scriptx/auth/login', { auth: false, body: { username, password } });
|
|
40
|
+
this.token = d.token;
|
|
41
|
+
config.setToken(this.token, username);
|
|
42
|
+
return d.user || {};
|
|
43
|
+
}
|
|
44
|
+
async register(username, password, email) {
|
|
45
|
+
const body = { username, password };
|
|
46
|
+
if (email) body.email = email;
|
|
47
|
+
const d = await this._req('POST', '/scriptx/auth/register', { auth: false, body });
|
|
48
|
+
this.token = d.token;
|
|
49
|
+
config.setToken(this.token, username);
|
|
50
|
+
return d.user || {};
|
|
51
|
+
}
|
|
52
|
+
async me() { return (await this._req('GET', '/scriptx/auth/me')).user || {}; }
|
|
53
|
+
async creds() { try { return await this._req('GET', '/scriptx/otpcart-creds'); } catch { return {}; } }
|
|
54
|
+
|
|
55
|
+
// ── campaign lifecycle ──
|
|
56
|
+
async createCampaign({ name, provider, count, concurrency, checkMinutes = false, tempotpServiceId = '940', deepCheck = false }) {
|
|
57
|
+
const d = await this._req('POST', '/scriptx/campaigns', {
|
|
58
|
+
body: { name, provider, count, concurrency, check_minutes: checkMinutes,
|
|
59
|
+
tempotp_service_id: tempotpServiceId, deep_check: deepCheck, local: true },
|
|
60
|
+
});
|
|
61
|
+
return d.data.id;
|
|
62
|
+
}
|
|
63
|
+
async markRunning(cid) { try { await this._req('POST', `/scriptx/campaigns/${cid}/mark-running`); } catch { /* */ } }
|
|
64
|
+
async ingestAccount(cid, payload) {
|
|
65
|
+
return this._req('POST', `/scriptx/campaigns/${cid}/accounts`, { body: payload });
|
|
66
|
+
}
|
|
67
|
+
async finishCampaign(cid, stats) {
|
|
68
|
+
return this._req('POST', `/scriptx/campaigns/${cid}/finish`, { body: stats });
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ── management (parity with the web dashboard) ──
|
|
72
|
+
async listCampaigns() { return (await this._req('GET', '/scriptx/campaigns')).data || []; }
|
|
73
|
+
async getCampaign(cid) { const d = await this._req('GET', `/scriptx/campaigns/${cid}`); return { campaign: d.data || {}, accounts: d.accounts || [] }; }
|
|
74
|
+
async exportCampaign(cid) { return (await this._req('GET', `/scriptx/campaigns/${cid}/export`)).accounts || []; }
|
|
75
|
+
async stopCampaign(cid) { return this._req('POST', `/scriptx/campaigns/${cid}/stop`); }
|
|
76
|
+
async deleteCampaign(cid) { return this._req('DELETE', `/scriptx/campaigns/${cid}`); }
|
|
77
|
+
|
|
78
|
+
// ── server-side creds ──
|
|
79
|
+
async saveCreds(provider, { email, password, apiKey } = {}) {
|
|
80
|
+
const body = { provider };
|
|
81
|
+
if (email != null) body.email = email;
|
|
82
|
+
if (password != null) body.password = password;
|
|
83
|
+
if (apiKey != null) body.api_key = apiKey;
|
|
84
|
+
return this._req('POST', '/scriptx/otpcart-creds', { body });
|
|
85
|
+
}
|
|
86
|
+
async testCreds(provider) { return this._req('POST', '/scriptx/otpcart-creds/test', { body: { provider } }); }
|
|
87
|
+
async serverBalance(provider) { return this._req('GET', `/scriptx/balance?provider=${provider}`); }
|
|
88
|
+
async tempotpServices() { return this._req('GET', '/scriptx/tempotp-services'); }
|
|
89
|
+
}
|
package/src/commands.js
ADDED
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
// Command implementations, written against an `io` interface so they work BOTH in the
|
|
2
|
+
// interactive ink shell (io = in-tree prompts/output) and one-shot CLI mode (io = plain
|
|
3
|
+
// stdin/stdout). Each command: async (io, api, args) => void.
|
|
4
|
+
import * as config from './config.js';
|
|
5
|
+
import { ApiClient, AuthError } from './api.js';
|
|
6
|
+
import { saveSessions } from './util.js';
|
|
7
|
+
import * as tp from './providers/tempotp.js';
|
|
8
|
+
import * as oc from './providers/otpcart.js';
|
|
9
|
+
import { Worker } from './worker.js';
|
|
10
|
+
import { RunController } from './controller.js';
|
|
11
|
+
import { STATUS } from './theme.js';
|
|
12
|
+
|
|
13
|
+
async function getApi(io) {
|
|
14
|
+
const api = new ApiClient();
|
|
15
|
+
if (!api.token) return loginFlow(io, api);
|
|
16
|
+
try { await api.me(); return api; }
|
|
17
|
+
catch (e) {
|
|
18
|
+
if (e instanceof AuthError) { io.print(' ! session expired'); config.clearToken(); return loginFlow(io, api); }
|
|
19
|
+
throw e;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async function loginFlow(io, api) {
|
|
24
|
+
api = api || new ApiClient();
|
|
25
|
+
io.print(` ◉ Sign in · ${api.base}`);
|
|
26
|
+
const username = await io.ask('username');
|
|
27
|
+
const password = await io.ask('password', { mask: true });
|
|
28
|
+
const user = await api.login(username, password);
|
|
29
|
+
io.print(` ✓ Welcome, ${user.username || username}`);
|
|
30
|
+
if (io.setUser) io.setUser(user.username || username);
|
|
31
|
+
return api;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// mask a secret for display: keep first 4 + last 4
|
|
35
|
+
function maskSecret(s) {
|
|
36
|
+
if (!s) return '';
|
|
37
|
+
if (s.length <= 8) return '••••';
|
|
38
|
+
return s.slice(0, 4) + '…' + s.slice(-4);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// ── provider setup (shared by run) ──
|
|
42
|
+
async function buildProvider(io, name) {
|
|
43
|
+
const cfg = config.load();
|
|
44
|
+
if (name === 'tempotp') {
|
|
45
|
+
let key = cfg.tempotp_api_key;
|
|
46
|
+
// If a key is saved locally, show it (masked) and let the user pick: use it or add new.
|
|
47
|
+
if (key) {
|
|
48
|
+
const choice = await io.select(`saved TempOTP key: ${maskSecret(key)}`, [
|
|
49
|
+
{ label: 'use saved', value: 'use', description: maskSecret(key) },
|
|
50
|
+
{ label: 'add new', value: 'new', description: 'enter a different key' }]);
|
|
51
|
+
if ((choice.value || choice) === 'new') key = null;
|
|
52
|
+
}
|
|
53
|
+
let freshlyEntered = false;
|
|
54
|
+
if (!key) { key = await io.ask('TempOTP API key'); freshlyEntered = true; }
|
|
55
|
+
|
|
56
|
+
const bal = await tp.balance(key);
|
|
57
|
+
if (bal == null) throw new Error('TempOTP key invalid or unreachable');
|
|
58
|
+
io.print(` ✓ TempOTP balance: ₹${bal}`);
|
|
59
|
+
if (freshlyEntered && await io.confirm('save this key locally for next time?', true)) config.setMany({ tempotp_api_key: key });
|
|
60
|
+
|
|
61
|
+
const svc = await io.select('service id', Object.keys(tp.SERVICES).map((id) => ({ label: id, value: id, description: `₹${tp.SERVICES[id]}` })));
|
|
62
|
+
return new tp.TempOTPProvider(key, svc.value || svc);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// OTPCart: same use-saved / add-new flow
|
|
66
|
+
let email = cfg.otpcart_email, password = cfg.otpcart_password;
|
|
67
|
+
if (email && password) {
|
|
68
|
+
const choice = await io.select(`saved OTPCart: ${email}`, [
|
|
69
|
+
{ label: 'use saved', value: 'use', description: email },
|
|
70
|
+
{ label: 'add new', value: 'new', description: 'enter different creds' }]);
|
|
71
|
+
if ((choice.value || choice) === 'new') { email = null; password = null; }
|
|
72
|
+
}
|
|
73
|
+
let freshlyEntered = false;
|
|
74
|
+
if (!email || !password) {
|
|
75
|
+
email = await io.ask('OTPCart email');
|
|
76
|
+
password = await io.ask('OTPCart password', { mask: true });
|
|
77
|
+
freshlyEntered = true;
|
|
78
|
+
}
|
|
79
|
+
const jwt = await oc.login(email, password);
|
|
80
|
+
io.print(' ✓ OTPCart connected');
|
|
81
|
+
if (freshlyEntered && await io.confirm('save these creds locally for next time?', true)) config.setMany({ otpcart_email: email, otpcart_password: password });
|
|
82
|
+
const deep = await io.confirm('deep number check?', false);
|
|
83
|
+
return new oc.OTPCartProvider(jwt, deep);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export async function run(io, api, args = {}) {
|
|
87
|
+
api = api || await getApi(io);
|
|
88
|
+
const pSel = args.provider || (await io.select('provider', [
|
|
89
|
+
{ label: 'otpcart', value: 'otpcart', description: 'email/password · WebSocket OTP' },
|
|
90
|
+
{ label: 'tempotp', value: 'tempotp', description: 'API key · HTTP poll' }])).value;
|
|
91
|
+
const provider = await buildProvider(io, pSel);
|
|
92
|
+
const count = args.count || Number(await io.ask('how many JSONs?', { dflt: '5' }));
|
|
93
|
+
const concurrency = args.concurrency || Number(await io.ask('concurrency (keep low — one IP)', { dflt: String(config.get('default_concurrency', 2)) }));
|
|
94
|
+
const checkMinutes = args.checkMinutes != null ? args.checkMinutes : await io.confirm('check Minutes (₹100 coupon)?', false);
|
|
95
|
+
const name = args.name || `ScripterX-${Math.floor(Date.now() / 1000)}`;
|
|
96
|
+
|
|
97
|
+
if (!(await io.confirm(`start ${count} extraction(s) at concurrency ${concurrency}?`, true))) return;
|
|
98
|
+
|
|
99
|
+
const cid = await api.createCampaign({ name, provider: pSel, count, concurrency, checkMinutes,
|
|
100
|
+
tempotpServiceId: provider.serviceId || '940', deepCheck: provider.deepCheck || false });
|
|
101
|
+
io.print(' ✓ campaign created · syncing to your dashboard');
|
|
102
|
+
|
|
103
|
+
const controller = new RunController({ name, provider: pSel, requested: count });
|
|
104
|
+
const worker = new Worker(api, provider, { campaignId: cid, requested: count, concurrency, checkMinutes,
|
|
105
|
+
onEvent: controller.handleEvent,
|
|
106
|
+
// on a failed/rejected OTP: pause, show the real reason, ask continue-or-stop
|
|
107
|
+
onFailure: async ({ mobile, reason }) => {
|
|
108
|
+
io.print(` ✗ ${mobile.slice(-10)} — ${reason}`, 'danger');
|
|
109
|
+
return io.confirm('OTP failed. Buy a new number and keep going?', true);
|
|
110
|
+
} });
|
|
111
|
+
|
|
112
|
+
if (io.startRun) io.startRun(controller); // mount the live view (interactive)
|
|
113
|
+
const onSigint = () => worker.stop();
|
|
114
|
+
process.on('SIGINT', onSigint);
|
|
115
|
+
const stats = await worker.run();
|
|
116
|
+
process.off('SIGINT', onSigint);
|
|
117
|
+
if (io.endRun) io.endRun();
|
|
118
|
+
|
|
119
|
+
io.print(` ✓ done — ${stats.succeeded} succeeded · ${stats.failed} failed · ${stats.cancelled} cancelled · ₹${stats.charges} spent`);
|
|
120
|
+
if (stats.succeeded > 0) {
|
|
121
|
+
let saved = null, lastErr = null;
|
|
122
|
+
for (let a = 0; a < 5 && !saved; a++) {
|
|
123
|
+
if (a > 0) await new Promise((r) => setTimeout(r, 1000));
|
|
124
|
+
try { saved = await saveSessions(api, cid, { campaignName: name }); } catch (e) { lastErr = e; }
|
|
125
|
+
}
|
|
126
|
+
if (saved) { io.print(` ✓ saved ${saved.count} session(s) to:`); io.print(` ${saved.path}`, 'accent'); }
|
|
127
|
+
else io.print(` ! couldn't auto-save${lastErr ? ` (${lastErr.message})` : ''} — run \`export ${name}\` to retry`);
|
|
128
|
+
} else io.print(' ◉ no sessions extracted — nothing to save.');
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export async function campaigns(io, api) {
|
|
132
|
+
api = api || await getApi(io);
|
|
133
|
+
const camps = await api.listCampaigns();
|
|
134
|
+
if (!camps.length) { io.print(' ◉ No campaigns yet. Type `run`.'); return; }
|
|
135
|
+
io.print(' ' + 'name'.padEnd(20) + 'provider'.padEnd(10) + 'status'.padEnd(13) + '✓ ✗ ⊘ ₹ created');
|
|
136
|
+
for (const c of camps.slice(0, 30)) {
|
|
137
|
+
const st = STATUS[c.status] || STATUS.pending;
|
|
138
|
+
io.print(' ' + (c.name || '—').slice(0, 18).padEnd(20) + (c.provider || '—').padEnd(10) +
|
|
139
|
+
`${st.glyph} ${c.status}`.padEnd(13) + `${c.succeeded || 0} ${c.failed || 0} ${c.cancelled || 0} ₹${c.charges_deducted || 0}`.padEnd(12) +
|
|
140
|
+
(c.created_at || '').slice(0, 10));
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async function pickCampaign(io, api, partial) {
|
|
145
|
+
const camps = await api.listCampaigns();
|
|
146
|
+
if (!camps.length) throw new Error('no campaigns');
|
|
147
|
+
if (partial) {
|
|
148
|
+
const m = camps.find((c) => c.id === partial || c.id.startsWith(partial) || c.name === partial);
|
|
149
|
+
if (m) return m;
|
|
150
|
+
throw new Error(`no campaign matching '${partial}'`);
|
|
151
|
+
}
|
|
152
|
+
// interactive picker
|
|
153
|
+
const sel = await io.select('which campaign?', camps.slice(0, 20).map((c) => ({ label: c.name || c.id.slice(0, 8), value: c.id, description: `${c.succeeded || 0} ok · ₹${c.charges_deducted || 0}` })));
|
|
154
|
+
return camps.find((c) => c.id === (sel.value || sel));
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export async function exportCmd(io, api, args = {}) {
|
|
158
|
+
api = api || await getApi(io);
|
|
159
|
+
const c = await pickCampaign(io, api, args.campaign);
|
|
160
|
+
let saved = null, lastErr = null;
|
|
161
|
+
for (let a = 0; a < 3 && !saved; a++) {
|
|
162
|
+
if (a > 0) await new Promise((r) => setTimeout(r, 800));
|
|
163
|
+
try { saved = await saveSessions(api, c.id, { campaignName: c.name, out: args.out }); } catch (e) { lastErr = e; }
|
|
164
|
+
}
|
|
165
|
+
if (saved) { io.print(` ✓ exported ${saved.count} session(s) to:`); io.print(` ${saved.path}`, 'accent'); }
|
|
166
|
+
else if (lastErr) io.print(` ✗ ${lastErr.message}`);
|
|
167
|
+
else io.print(' ! no extracted sessions in this campaign yet.');
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export async function balance(io, api, args = {}) {
|
|
171
|
+
api = api || await getApi(io);
|
|
172
|
+
for (const p of args.provider ? [args.provider] : ['otpcart', 'tempotp']) {
|
|
173
|
+
try { const r = await api.serverBalance(p); io.print(r.balance != null ? ` ✓ ${p}: ₹${r.balance}` : ` ◉ ${p}: ${r.note || 'no balance'}`); }
|
|
174
|
+
catch (e) { io.print(` ! ${p}: ${e.message}`); }
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export async function creds(io, api, args = {}) {
|
|
179
|
+
api = api || await getApi(io);
|
|
180
|
+
const action = args.action || (await io.select('credentials', [
|
|
181
|
+
{ label: 'show', value: 'show', description: 'what is saved' },
|
|
182
|
+
{ label: 'save', value: 'save', description: 'store provider creds' },
|
|
183
|
+
{ label: 'test', value: 'test', description: 'verify creds' }])).value;
|
|
184
|
+
if (action === 'show') {
|
|
185
|
+
const r = await api.creds();
|
|
186
|
+
for (const p of ['otpcart', 'tempotp']) io.print(` ◉ ${p}: ${r[p]?.has_creds ? '✓ saved' : '— not set'}`);
|
|
187
|
+
} else if (action === 'save') {
|
|
188
|
+
const provider = (await io.select('provider', [{ label: 'otpcart', value: 'otpcart' }, { label: 'tempotp', value: 'tempotp' }])).value;
|
|
189
|
+
if (provider === 'tempotp') await api.saveCreds('tempotp', { apiKey: await io.ask('TempOTP API key') });
|
|
190
|
+
else await api.saveCreds('otpcart', { email: await io.ask('OTPCart email'), password: await io.ask('OTPCart password', { mask: true }) });
|
|
191
|
+
io.print(` ✓ ${provider} credentials saved.`);
|
|
192
|
+
} else {
|
|
193
|
+
const provider = (await io.select('provider', [{ label: 'otpcart', value: 'otpcart' }, { label: 'tempotp', value: 'tempotp' }])).value;
|
|
194
|
+
try { const r = await api.testCreds(provider); io.print(` ✓ ${provider} verified${r.balance != null ? ` · ₹${r.balance}` : ''}`); }
|
|
195
|
+
catch (e) { io.print(` ✗ ${e.message}`); }
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export async function stop(io, api, args = {}) {
|
|
200
|
+
api = api || await getApi(io);
|
|
201
|
+
const c = await pickCampaign(io, api, args.campaign);
|
|
202
|
+
await api.stopCampaign(c.id); io.print(' ✓ stop requested.');
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export async function del(io, api, args = {}) {
|
|
206
|
+
api = api || await getApi(io);
|
|
207
|
+
const c = await pickCampaign(io, api, args.campaign);
|
|
208
|
+
if (!(await io.confirm(`delete '${c.name}'?`, false))) return;
|
|
209
|
+
await api.deleteCampaign(c.id); io.print(' ✓ deleted.');
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export async function whoami(io) {
|
|
213
|
+
const api = new ApiClient();
|
|
214
|
+
if (!api.token) { io.print(' ! not signed in — type `login`'); return; }
|
|
215
|
+
try { const u = await api.me(); io.print(` ✓ ${u.username} · ${api.base}`); } catch (e) { io.print(` ✗ ${e.message}`); }
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export async function login(io) { await loginFlow(io); }
|
|
219
|
+
export function logout(io) { config.clearToken(); if (io.setUser) io.setUser(null); io.print(' ✓ signed out.'); }
|
|
220
|
+
|
|
221
|
+
export async function configCmd(io, _api, args = {}) {
|
|
222
|
+
const cfg = config.load();
|
|
223
|
+
const action = args.action || (await io.select('config', [
|
|
224
|
+
{ label: 'show', value: 'show' },
|
|
225
|
+
{ label: 'server', value: 'server', description: 'change backend URL' },
|
|
226
|
+
{ label: 'concurrency', value: 'concurrency', description: 'default parallelism' }])).value;
|
|
227
|
+
if (action === 'show') {
|
|
228
|
+
for (const k of ['server_base_url', 'username', 'default_concurrency']) io.print(` ${k.padEnd(20)} ${cfg[k]}`);
|
|
229
|
+
io.print(` ${'logged_in'.padEnd(20)} ${cfg.jwt ? 'yes' : 'no'}`);
|
|
230
|
+
} else if (action === 'server') {
|
|
231
|
+
const v = args.value || await io.ask('server URL', { dflt: cfg.server_base_url });
|
|
232
|
+
config.setMany({ server_base_url: v }); io.print(` ✓ server → ${v}`);
|
|
233
|
+
} else if (action === 'concurrency') {
|
|
234
|
+
const v = args.value || await io.ask('default concurrency', { dflt: String(cfg.default_concurrency) });
|
|
235
|
+
config.setMany({ default_concurrency: parseInt(v, 10) }); io.print(` ✓ concurrency → ${v}`);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
export function help(io) {
|
|
240
|
+
io.print(' commands: run · campaigns · export · balance · creds · stop · delete · whoami · config · logout · exit');
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// dispatch table used by the interactive shell
|
|
244
|
+
export const REGISTRY = {
|
|
245
|
+
run, campaigns, export: exportCmd, balance, creds, stop, delete: del,
|
|
246
|
+
whoami, login, logout, config: configCmd, help,
|
|
247
|
+
};
|
package/src/config.js
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// Local config + token store at ~/.scripterx/config.json (0600).
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { mkdirSync, readFileSync, writeFileSync, existsSync, chmodSync } from 'node:fs';
|
|
5
|
+
|
|
6
|
+
export const CONFIG_DIR = join(homedir(), '.scripterx');
|
|
7
|
+
export const CONFIG_FILE = join(CONFIG_DIR, 'config.json');
|
|
8
|
+
|
|
9
|
+
const DEFAULT_SERVER = process.env.SCRIPTERX_SERVER || 'https://api.marthunt.tech';
|
|
10
|
+
|
|
11
|
+
const DEFAULTS = {
|
|
12
|
+
server_base_url: DEFAULT_SERVER,
|
|
13
|
+
jwt: null,
|
|
14
|
+
username: null,
|
|
15
|
+
tempotp_api_key: null,
|
|
16
|
+
otpcart_email: null,
|
|
17
|
+
otpcart_password: null,
|
|
18
|
+
default_concurrency: 2,
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
function ensureDir() {
|
|
22
|
+
if (!existsSync(CONFIG_DIR)) mkdirSync(CONFIG_DIR, { mode: 0o700, recursive: true });
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function load() {
|
|
26
|
+
ensureDir();
|
|
27
|
+
let data = {};
|
|
28
|
+
if (existsSync(CONFIG_FILE)) {
|
|
29
|
+
try { data = JSON.parse(readFileSync(CONFIG_FILE, 'utf8')); } catch { data = {}; }
|
|
30
|
+
}
|
|
31
|
+
const merged = { ...DEFAULTS, ...data };
|
|
32
|
+
if (process.env.SCRIPTERX_SERVER) merged.server_base_url = process.env.SCRIPTERX_SERVER;
|
|
33
|
+
return merged;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function save(cfg) {
|
|
37
|
+
ensureDir();
|
|
38
|
+
writeFileSync(CONFIG_FILE, JSON.stringify(cfg, null, 2));
|
|
39
|
+
try { chmodSync(CONFIG_FILE, 0o600); } catch { /* ignore */ }
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function get(key, dflt = null) {
|
|
43
|
+
const v = load()[key];
|
|
44
|
+
return v === undefined ? dflt : v;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function setMany(kv) {
|
|
48
|
+
const cfg = load();
|
|
49
|
+
Object.assign(cfg, kv);
|
|
50
|
+
save(cfg);
|
|
51
|
+
return cfg;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export const getToken = () => load().jwt;
|
|
55
|
+
|
|
56
|
+
export function setToken(token, username) {
|
|
57
|
+
const cfg = load();
|
|
58
|
+
cfg.jwt = token;
|
|
59
|
+
if (username) cfg.username = username;
|
|
60
|
+
save(cfg);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function clearToken() {
|
|
64
|
+
const cfg = load();
|
|
65
|
+
cfg.jwt = null;
|
|
66
|
+
save(cfg);
|
|
67
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// RunController — shared, subscribable state for a live extraction run.
|
|
2
|
+
// The Worker writes via onEvent(); RunView subscribes and re-renders on each event.
|
|
3
|
+
// Decouples the worker from the UI and (crucially) makes updates actually propagate.
|
|
4
|
+
export class RunController {
|
|
5
|
+
constructor({ name, provider, requested }) {
|
|
6
|
+
this.name = name;
|
|
7
|
+
this.provider = provider;
|
|
8
|
+
this.requested = requested;
|
|
9
|
+
this.stats = { total: requested, succeeded: 0, failed: 0, cancelled: 0, generated: 0, attempts: 0, charges: 0 };
|
|
10
|
+
this.slots = {};
|
|
11
|
+
this.rows = [];
|
|
12
|
+
this._subs = new Set();
|
|
13
|
+
this.onFinish = null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// the callback the Worker is given
|
|
17
|
+
handleEvent = (kind, payload) => {
|
|
18
|
+
if (kind === 'slot') this.slots[payload.slot] = { ...payload };
|
|
19
|
+
else if (kind === 'progress' || kind === 'done') this.stats = { ...payload };
|
|
20
|
+
else if (kind === 'row') this.rows.push(payload);
|
|
21
|
+
this._emit(kind, payload);
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
subscribe(fn) { this._subs.add(fn); }
|
|
25
|
+
unsubscribe(fn) { this._subs.delete(fn); }
|
|
26
|
+
_emit(kind, payload) { for (const fn of this._subs) { try { fn(kind, payload); } catch { /* */ } } }
|
|
27
|
+
}
|