@ai-digital-factory/lmt-cli 0.1.6 → 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 +175 -51
- package/dist/chunk-4N3XUBM4.js +1483 -0
- package/dist/chunk-7J2U4G5L.js +1394 -0
- package/dist/chunk-AQIOT6YD.js +2799 -0
- package/dist/chunk-DUF5HVSQ.js +58 -0
- package/dist/controller-app-UTLE4YVF.js +1382 -0
- package/dist/credential-app-QEFV6LZM.js +6 -0
- package/dist/index.js +8 -3544
- package/dist/menu-actions-HV2AY4KJ.js +67 -0
- package/dist/src-MZYWVU46.js +380 -0
- package/dist/web-5HW6PMSA.js +9 -0
- package/package.json +2 -1
|
@@ -0,0 +1,1382 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CredentialProvisionApp
|
|
3
|
+
} from "./chunk-DUF5HVSQ.js";
|
|
4
|
+
import {
|
|
5
|
+
isCandleExportDate,
|
|
6
|
+
parseCandleExportInstrument
|
|
7
|
+
} from "./chunk-7J2U4G5L.js";
|
|
8
|
+
import {
|
|
9
|
+
ENTRY_CATEGORIES,
|
|
10
|
+
isLoginRequired,
|
|
11
|
+
isOperatorMarketProtection,
|
|
12
|
+
sessionUsable
|
|
13
|
+
} from "./chunk-4N3XUBM4.js";
|
|
14
|
+
|
|
15
|
+
// src/controller-app.tsx
|
|
16
|
+
import { useEffect as useEffect2, useMemo, useRef as useRef3, useState as useState3 } from "react";
|
|
17
|
+
import { Box as Box3, Text as Text3, useApp, useInput as useInput3 } from "ink";
|
|
18
|
+
|
|
19
|
+
// src/command-menu.tsx
|
|
20
|
+
import { access } from "node:fs/promises";
|
|
21
|
+
import path from "node:path";
|
|
22
|
+
import { useEffect, useRef as useRef2, useState as useState2 } from "react";
|
|
23
|
+
import { Box as Box2, Text as Text2, useInput as useInput2 } from "ink";
|
|
24
|
+
|
|
25
|
+
// src/menu-cursor.ts
|
|
26
|
+
function cursorStep(key) {
|
|
27
|
+
return key.upArrow ? -1 : key.downArrow ? 1 : 0;
|
|
28
|
+
}
|
|
29
|
+
function wrapCursor(index, step, length) {
|
|
30
|
+
return (index + step + length) % length;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// src/web-app.tsx
|
|
34
|
+
import { useRef, useState } from "react";
|
|
35
|
+
import { Box, Text, useInput } from "ink";
|
|
36
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
37
|
+
var ACT = "Act on a control";
|
|
38
|
+
var FRESH = "Fresh login test";
|
|
39
|
+
var WEB_MENU = [
|
|
40
|
+
{ command: "start", path: ["Start browser"] },
|
|
41
|
+
{ command: "inspect", path: ["Inspect"] },
|
|
42
|
+
{ command: "inspect", path: ["Inspect full page"], fullPage: true },
|
|
43
|
+
{ command: "click", path: [ACT, "Click"] },
|
|
44
|
+
{ command: "fill", path: [ACT, "Fill"] },
|
|
45
|
+
{ command: "read", path: [ACT, "Read"] },
|
|
46
|
+
{ command: "select", path: [ACT, "Select"] },
|
|
47
|
+
{ command: "paste", path: [ACT, "Paste"] },
|
|
48
|
+
{ command: "check", path: [ACT, "Check"] },
|
|
49
|
+
{ command: "clipboard-check", path: [ACT, "Clipboard-check"] },
|
|
50
|
+
{ command: "scroll", path: [ACT, "Scroll"] },
|
|
51
|
+
{ command: "clipboard", path: ["Clipboard"] },
|
|
52
|
+
{ command: "navigate", path: ["Navigate"] },
|
|
53
|
+
{ command: "scroll", path: ["Scroll"] },
|
|
54
|
+
{ command: "close", path: ["Close"] },
|
|
55
|
+
{ command: "close", path: ["Close with full-page screenshot"], fullPage: true },
|
|
56
|
+
{ command: "stop", path: ["Stop"] },
|
|
57
|
+
{ command: "operator-login", path: [FRESH, "Operator login"] },
|
|
58
|
+
{ command: "operator-logout", path: [FRESH, "Operator logout"] },
|
|
59
|
+
{ command: "zerodha-login", path: [FRESH, "Zerodha login"] },
|
|
60
|
+
{ command: "zerodha-otp", path: [FRESH, "Zerodha OTP"] },
|
|
61
|
+
{ command: "zerodha-cancel", path: [FRESH, "Zerodha cancel"] },
|
|
62
|
+
{ command: "zerodha-logout", path: [FRESH, "Zerodha logout"] },
|
|
63
|
+
{ command: "install-browser", path: ["Install browser"] }
|
|
64
|
+
];
|
|
65
|
+
var SECRET_LABEL = /password|passcode|secret|token|otp|\bpin\b|api key/i;
|
|
66
|
+
var SCROLL_STEP = 400;
|
|
67
|
+
var PAGE_STEP = 800;
|
|
68
|
+
function resultLines(result, origin) {
|
|
69
|
+
const lines = [];
|
|
70
|
+
if (result.outcome !== "success") lines.push(`Outcome: ${result.outcome}`);
|
|
71
|
+
if (result.error) lines.push(`Error: ${result.error.code}: ${result.error.message}`);
|
|
72
|
+
if (result.sessionId) lines.push(`Session: ${result.sessionId}${result.device ? ` (${result.device.mode})` : ""}`);
|
|
73
|
+
if (result.visibleState) lines.push(`Title: ${result.visibleState.title}`);
|
|
74
|
+
if (result.url) lines.push(`URL: ${result.url}`);
|
|
75
|
+
lines.push(`Origin: ${result.origin ?? origin}`);
|
|
76
|
+
if (result.authentication.status !== "unknown") lines.push(`Authentication: ${result.authentication.status}`);
|
|
77
|
+
if (result.secret) {
|
|
78
|
+
const secret = result.secret;
|
|
79
|
+
lines.push(secret.present ? `Secret: present, length ${secret.length}, fingerprint ${secret.fingerprint}` : "Secret: absent");
|
|
80
|
+
} else if (result.value !== void 0) lines.push(`Value: ${result.value}`);
|
|
81
|
+
if (result.matches !== void 0) lines.push(`Matches: ${result.matches ? "yes" : "no"}`);
|
|
82
|
+
if (result.loginAttempt) lines.push(`Zerodha login attempt: ${result.loginAttempt.status}`);
|
|
83
|
+
lines.push(result.screenshot.outcome === "captured" ? `Screenshot: ${result.screenshot.path}` : `Screenshot: ${result.screenshot.outcome}${"reason" in result.screenshot ? ` (${result.screenshot.reason})` : ""}`);
|
|
84
|
+
return lines;
|
|
85
|
+
}
|
|
86
|
+
function controlLines(controls) {
|
|
87
|
+
return controls.map((control, index) => `${index + 1}. [${control.kind}] ${control.label}`);
|
|
88
|
+
}
|
|
89
|
+
function WebApp(props) {
|
|
90
|
+
const { actions, origin } = props;
|
|
91
|
+
const [screen, setScreen] = useState("menu");
|
|
92
|
+
const [index, setIndex] = useState(0);
|
|
93
|
+
const [busy, setBusy] = useState("");
|
|
94
|
+
const [lines, setLines] = useState([]);
|
|
95
|
+
const [error, setError] = useState("");
|
|
96
|
+
const [current, setCurrent] = useState(void 0);
|
|
97
|
+
const [controls, setControls] = useState([]);
|
|
98
|
+
const [chosen, setChosen] = useState(void 0);
|
|
99
|
+
const [live, setLive] = useState([]);
|
|
100
|
+
const [pendingStart, setPendingStart] = useState({ fresh: false });
|
|
101
|
+
const [attemptId, setAttemptId] = useState(void 0);
|
|
102
|
+
const [prompt, setPrompt] = useState(void 0);
|
|
103
|
+
const typed = useRef("");
|
|
104
|
+
const [typedLength, setTypedLength] = useState(0);
|
|
105
|
+
const [typedVisible, setTypedVisible] = useState("");
|
|
106
|
+
const [secret, setSecret] = useState(false);
|
|
107
|
+
const [scrollReference, setScrollReference] = useState(void 0);
|
|
108
|
+
const [digits, setDigits] = useState("");
|
|
109
|
+
function go(next, nextIndex = 0) {
|
|
110
|
+
setIndex(nextIndex);
|
|
111
|
+
setDigits("");
|
|
112
|
+
setScreen(next);
|
|
113
|
+
}
|
|
114
|
+
function show(result, nextScreen = current?.fresh || pendingStart.fresh ? "fresh" : "menu") {
|
|
115
|
+
const stale = result.error?.code === "stale_reference";
|
|
116
|
+
const nextControls = !stale && result.visibleState ? result.visibleState.controls : [];
|
|
117
|
+
setControls(nextControls);
|
|
118
|
+
if (stale) setChosen(void 0);
|
|
119
|
+
setLines([
|
|
120
|
+
...resultLines(result, origin),
|
|
121
|
+
...stale ? [] : controlLines(nextControls)
|
|
122
|
+
]);
|
|
123
|
+
setError(stale ? "The page changed since the last inspect. Choose Inspect to get current controls before acting again." : "");
|
|
124
|
+
go(nextScreen);
|
|
125
|
+
}
|
|
126
|
+
async function run(label, work, after) {
|
|
127
|
+
setBusy(label);
|
|
128
|
+
setScreen("busy");
|
|
129
|
+
try {
|
|
130
|
+
const result = await work();
|
|
131
|
+
after?.(result);
|
|
132
|
+
show(result, current?.fresh ? "fresh" : "menu");
|
|
133
|
+
} catch (cause) {
|
|
134
|
+
setError(cause instanceof Error ? cause.message : "The web command failed.");
|
|
135
|
+
go(current?.fresh ? "fresh" : "menu");
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
function needSession() {
|
|
139
|
+
if (current) return current.sessionId;
|
|
140
|
+
setError("Start a browser or choose one from Sessions first.");
|
|
141
|
+
return void 0;
|
|
142
|
+
}
|
|
143
|
+
function send(action, label, fullPage, after) {
|
|
144
|
+
const sessionId = needSession();
|
|
145
|
+
if (!sessionId) return;
|
|
146
|
+
void run(label, () => actions.request(sessionId, action, fullPage), after);
|
|
147
|
+
}
|
|
148
|
+
function ask(next) {
|
|
149
|
+
typed.current = "";
|
|
150
|
+
setTypedLength(0);
|
|
151
|
+
setTypedVisible("");
|
|
152
|
+
setSecret(next.masked);
|
|
153
|
+
setPrompt(next);
|
|
154
|
+
setError("");
|
|
155
|
+
go("text");
|
|
156
|
+
}
|
|
157
|
+
async function start(mode, headless) {
|
|
158
|
+
const fresh = pendingStart.fresh;
|
|
159
|
+
setBusy(fresh ? "Starting an isolated fresh browser\u2026" : "Starting the browser with the Controller Session\u2026");
|
|
160
|
+
setScreen("busy");
|
|
161
|
+
try {
|
|
162
|
+
const result = await actions.start({ mode, headless, fresh });
|
|
163
|
+
if (result.outcome === "blocked") {
|
|
164
|
+
setLines([]);
|
|
165
|
+
setError("");
|
|
166
|
+
go("blocked");
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
if (result.sessionId && result.outcome !== "failure") {
|
|
170
|
+
setCurrent({ sessionId: result.sessionId, mode, fresh });
|
|
171
|
+
setAttemptId(void 0);
|
|
172
|
+
}
|
|
173
|
+
show(result, fresh ? "fresh" : "menu");
|
|
174
|
+
} catch (cause) {
|
|
175
|
+
setError(cause instanceof Error ? cause.message : "Could not start the browser.");
|
|
176
|
+
go(fresh ? "fresh" : "menu");
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
function chooseDevice(fresh) {
|
|
180
|
+
setPendingStart({ fresh });
|
|
181
|
+
setError("");
|
|
182
|
+
go("device", (props.device ?? "mobile") === "desktop" ? 1 : 0);
|
|
183
|
+
}
|
|
184
|
+
async function showSessions() {
|
|
185
|
+
setBusy("Listing live sessions\u2026");
|
|
186
|
+
setScreen("busy");
|
|
187
|
+
try {
|
|
188
|
+
setLive(await actions.sessions());
|
|
189
|
+
setError("");
|
|
190
|
+
go("sessions");
|
|
191
|
+
} catch (cause) {
|
|
192
|
+
setError(cause instanceof Error ? cause.message : "Could not list the live sessions.");
|
|
193
|
+
go("menu");
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
function ended(result) {
|
|
197
|
+
if (result.outcome === "success" || result.error?.code === "session_not_found") {
|
|
198
|
+
setCurrent(void 0);
|
|
199
|
+
setControls([]);
|
|
200
|
+
setAttemptId(void 0);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
function scroll(x, y) {
|
|
204
|
+
const sessionId = needSession();
|
|
205
|
+
if (!sessionId) return;
|
|
206
|
+
const reference = scrollReference?.reference;
|
|
207
|
+
setBusy(`Scrolling ${y ? `y ${y}` : `x ${x}`}\u2026`);
|
|
208
|
+
setScreen("busy");
|
|
209
|
+
void actions.request(sessionId, { action: "scroll", ...reference ? { reference } : {}, x, y }).then((result) => {
|
|
210
|
+
show(result, "scroll");
|
|
211
|
+
if (scrollReference && chosen !== void 0) {
|
|
212
|
+
const same = result.visibleState?.controls[chosen];
|
|
213
|
+
if (same && same.kind === scrollReference.kind && same.label === scrollReference.label) setScrollReference(same);
|
|
214
|
+
else {
|
|
215
|
+
setScrollReference(void 0);
|
|
216
|
+
setError("The chosen control changed; scrolling the page. Choose the control again to scroll it.");
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}, (cause) => {
|
|
220
|
+
setError(cause instanceof Error ? cause.message : "Scroll failed.");
|
|
221
|
+
go("menu");
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
const handlers = {
|
|
225
|
+
start: () => chooseDevice(false),
|
|
226
|
+
inspect: (entry) => send({ action: "inspect" }, "Inspecting\u2026", "fullPage" in entry),
|
|
227
|
+
click: () => controlAction((control) => send({ action: "click", reference: control.reference }, "Clicking\u2026")),
|
|
228
|
+
fill: () => controlAction((control) => ask({
|
|
229
|
+
title: `Fill ${control.label || control.kind}`,
|
|
230
|
+
masked: SECRET_LABEL.test(control.label),
|
|
231
|
+
toggleSecret: true,
|
|
232
|
+
submit: (value, isSecret) => send({ action: "fill", reference: control.reference, value, secret: isSecret }, "Filling\u2026")
|
|
233
|
+
})),
|
|
234
|
+
read: () => controlAction((control) => send({ action: "read", reference: control.reference }, "Reading\u2026")),
|
|
235
|
+
select: () => controlAction((control) => ask({
|
|
236
|
+
title: `Option for ${control.label || control.kind}`,
|
|
237
|
+
masked: false,
|
|
238
|
+
toggleSecret: false,
|
|
239
|
+
submit: (value) => send({ action: "select", reference: control.reference, value }, "Selecting\u2026")
|
|
240
|
+
})),
|
|
241
|
+
paste: () => controlAction((control) => send({ action: "paste", reference: control.reference }, "Pasting\u2026")),
|
|
242
|
+
check: () => controlAction((control) => ask({
|
|
243
|
+
title: `Expected value of ${control.label || control.kind}`,
|
|
244
|
+
masked: true,
|
|
245
|
+
toggleSecret: false,
|
|
246
|
+
submit: (expected) => send({ action: "check", reference: control.reference, expected }, "Checking\u2026")
|
|
247
|
+
})),
|
|
248
|
+
"clipboard-check": () => controlAction((control) => send({ action: "clipboard-check", reference: control.reference }, "Checking the clipboard\u2026")),
|
|
249
|
+
scroll: (entry) => {
|
|
250
|
+
if (!needSession()) return;
|
|
251
|
+
if (entry.path[0] === ACT) {
|
|
252
|
+
controlAction((control) => {
|
|
253
|
+
setScrollReference(control);
|
|
254
|
+
go("scroll");
|
|
255
|
+
});
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
setScrollReference(void 0);
|
|
259
|
+
go("scroll");
|
|
260
|
+
},
|
|
261
|
+
clipboard: () => send({ action: "clipboard" }, "Observing the clipboard\u2026"),
|
|
262
|
+
navigate: () => {
|
|
263
|
+
if (!needSession()) return;
|
|
264
|
+
ask({ title: "Path or URL", masked: false, toggleSecret: false, submit: (url) => send({ action: "navigate", url }, "Navigating\u2026") });
|
|
265
|
+
},
|
|
266
|
+
close: (entry) => send({ action: "close" }, "Closing\u2026", "fullPage" in entry, ended),
|
|
267
|
+
stop: () => send({ action: "stop" }, "Stopping\u2026", false, ended),
|
|
268
|
+
"operator-login": () => send({ action: "operator-login" }, "Operator login\u2026"),
|
|
269
|
+
"operator-logout": () => send({ action: "operator-logout" }, "Operator logout\u2026"),
|
|
270
|
+
"zerodha-login": () => send({ action: "zerodha-login" }, "Starting Zerodha login\u2026", false, (result) => {
|
|
271
|
+
const attempt = result.loginAttempt?.attemptId ?? ("attemptId" in result.authentication ? result.authentication.attemptId : void 0);
|
|
272
|
+
setAttemptId(attempt);
|
|
273
|
+
}),
|
|
274
|
+
"zerodha-otp": () => {
|
|
275
|
+
if (!needSession()) return;
|
|
276
|
+
if (!attemptId) {
|
|
277
|
+
setError("Choose Zerodha login first to start an attempt.");
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
ask({ title: "Zerodha OTP", masked: true, toggleSecret: false, digits: 6, submit: (otp) => {
|
|
281
|
+
if (!/^\d{6}$/.test(otp)) {
|
|
282
|
+
setError("Enter the 6-digit Zerodha OTP.");
|
|
283
|
+
go("fresh");
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
send({ action: "zerodha-otp", attemptId, otp }, "Verifying the Zerodha OTP\u2026");
|
|
287
|
+
} });
|
|
288
|
+
},
|
|
289
|
+
"zerodha-cancel": () => send({ action: "zerodha-cancel" }, "Cancelling Zerodha login\u2026", false, () => setAttemptId(void 0)),
|
|
290
|
+
"zerodha-logout": () => send({ action: "zerodha-logout" }, "Zerodha logout\u2026"),
|
|
291
|
+
"install-browser": () => {
|
|
292
|
+
setBusy("Installing Chromium\u2026");
|
|
293
|
+
setScreen("busy");
|
|
294
|
+
void actions.installBrowser().then(({ code, output }) => {
|
|
295
|
+
setLines([code === 0 ? "Chromium is installed." : "Chromium setup failed.", ...output ? output.split("\n") : []]);
|
|
296
|
+
setError("");
|
|
297
|
+
go("menu");
|
|
298
|
+
}, (cause) => {
|
|
299
|
+
setError(cause instanceof Error ? cause.message : "Could not run the browser installer.");
|
|
300
|
+
go("menu");
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
};
|
|
304
|
+
function controlAction(act) {
|
|
305
|
+
const control = chosen !== void 0 ? controls[chosen] : void 0;
|
|
306
|
+
if (!control) {
|
|
307
|
+
setError("Choose a control from the latest inspect first.");
|
|
308
|
+
go("controls");
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
act(control);
|
|
312
|
+
}
|
|
313
|
+
const entryItems = (filter, label) => WEB_MENU.filter(filter).map((entry) => ({ label: label(entry), run: () => {
|
|
314
|
+
setError("");
|
|
315
|
+
handlers[entry.command](entry);
|
|
316
|
+
} }));
|
|
317
|
+
const top = (entry) => entry.path.length === 1;
|
|
318
|
+
const menuItems = [
|
|
319
|
+
...entryItems((entry) => entry.command === "start", (entry) => entry.path[0]),
|
|
320
|
+
{
|
|
321
|
+
label: `${FRESH} (isolated \u2014 does not affect the Controller Session)`,
|
|
322
|
+
run: () => {
|
|
323
|
+
setError("");
|
|
324
|
+
if (current?.fresh) go("fresh");
|
|
325
|
+
else chooseDevice(true);
|
|
326
|
+
}
|
|
327
|
+
},
|
|
328
|
+
{ label: "Sessions", run: () => {
|
|
329
|
+
void showSessions();
|
|
330
|
+
} },
|
|
331
|
+
...entryItems((entry) => top(entry) && entry.command.startsWith("inspect"), (entry) => entry.path[0]),
|
|
332
|
+
{
|
|
333
|
+
label: ACT,
|
|
334
|
+
run: () => {
|
|
335
|
+
setError("");
|
|
336
|
+
if (!needSession()) return;
|
|
337
|
+
if (!controls.length) {
|
|
338
|
+
setError("Inspect first to list the page's controls.");
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
go("controls", chosen ?? 0);
|
|
342
|
+
}
|
|
343
|
+
},
|
|
344
|
+
...entryItems((entry) => top(entry) && !["start", "inspect"].includes(entry.command), (entry) => entry.path[0]),
|
|
345
|
+
{ label: "Back", run: props.onBack }
|
|
346
|
+
];
|
|
347
|
+
const controlActionItems = entryItems((entry) => entry.path[0] === ACT, (entry) => entry.path[1]);
|
|
348
|
+
const freshItems = [
|
|
349
|
+
...entryItems((entry) => entry.path[0] === FRESH, (entry) => entry.path[1]),
|
|
350
|
+
{ label: "Back", run: () => go("menu") }
|
|
351
|
+
];
|
|
352
|
+
const lists = {
|
|
353
|
+
menu: menuItems,
|
|
354
|
+
fresh: freshItems,
|
|
355
|
+
controlActions: controlActionItems,
|
|
356
|
+
device: ["mobile", "desktop"].map((mode) => ({
|
|
357
|
+
label: mode,
|
|
358
|
+
run: () => {
|
|
359
|
+
setPendingStart((value) => ({ ...value, mode }));
|
|
360
|
+
go("visibility");
|
|
361
|
+
}
|
|
362
|
+
})),
|
|
363
|
+
visibility: [
|
|
364
|
+
{ label: "Headless", run: () => {
|
|
365
|
+
void start(pendingStart.mode ?? "mobile", true);
|
|
366
|
+
} },
|
|
367
|
+
{ label: "Visible", run: () => {
|
|
368
|
+
void start(pendingStart.mode ?? "mobile", false);
|
|
369
|
+
} }
|
|
370
|
+
],
|
|
371
|
+
sessions: live.map((session) => ({
|
|
372
|
+
label: `${session.sessionId} \xB7 ${session.mode}${session.fresh ? " \xB7 fresh" : ""} \xB7 ${session.url ?? origin}`,
|
|
373
|
+
run: () => {
|
|
374
|
+
if (session.sessionId !== current?.sessionId) {
|
|
375
|
+
setControls([]);
|
|
376
|
+
setChosen(void 0);
|
|
377
|
+
setAttemptId(void 0);
|
|
378
|
+
}
|
|
379
|
+
setCurrent({ sessionId: session.sessionId, mode: session.mode, fresh: session.fresh });
|
|
380
|
+
setLines([`Current session: ${session.sessionId} (${session.mode})`]);
|
|
381
|
+
setError("");
|
|
382
|
+
go(session.fresh ? "fresh" : "menu");
|
|
383
|
+
}
|
|
384
|
+
})),
|
|
385
|
+
controls: controls.map((control, position) => ({
|
|
386
|
+
label: `${position + 1}. [${control.kind}] ${control.label}`,
|
|
387
|
+
run: () => {
|
|
388
|
+
setChosen(position);
|
|
389
|
+
go("controlActions");
|
|
390
|
+
}
|
|
391
|
+
}))
|
|
392
|
+
};
|
|
393
|
+
useInput((input, key) => {
|
|
394
|
+
if (screen === "busy") return;
|
|
395
|
+
if (screen === "blocked") {
|
|
396
|
+
if (key.return) props.onLogIn();
|
|
397
|
+
else if (key.escape) go("menu");
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
if (screen === "text" && prompt) {
|
|
401
|
+
if (key.escape) {
|
|
402
|
+
setPrompt(void 0);
|
|
403
|
+
go(current?.fresh ? "fresh" : "menu");
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
if (key.return) {
|
|
407
|
+
const value = typed.current;
|
|
408
|
+
typed.current = "";
|
|
409
|
+
setTypedLength(0);
|
|
410
|
+
setTypedVisible("");
|
|
411
|
+
setPrompt(void 0);
|
|
412
|
+
prompt.submit(value, secret);
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
if (key.tab && prompt.toggleSecret) {
|
|
416
|
+
setSecret((value) => !value);
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
let next = typed.current;
|
|
420
|
+
if (key.backspace || key.delete) next = next.slice(0, -1);
|
|
421
|
+
else if (input && !key.ctrl && !key.meta) next = prompt.digits ? (next + input.replace(/\D/g, "")).slice(0, prompt.digits) : next + input;
|
|
422
|
+
typed.current = next;
|
|
423
|
+
setTypedLength(next.length);
|
|
424
|
+
setTypedVisible(next);
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
if (screen === "scroll") {
|
|
428
|
+
if (key.escape || key.return) {
|
|
429
|
+
setScrollReference(void 0);
|
|
430
|
+
go("menu");
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
if (key.downArrow) scroll(0, SCROLL_STEP);
|
|
434
|
+
else if (key.upArrow) scroll(0, -SCROLL_STEP);
|
|
435
|
+
else if (key.pageDown) scroll(0, PAGE_STEP);
|
|
436
|
+
else if (key.pageUp) scroll(0, -PAGE_STEP);
|
|
437
|
+
else if (key.rightArrow) scroll(SCROLL_STEP, 0);
|
|
438
|
+
else if (key.leftArrow) scroll(-SCROLL_STEP, 0);
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
const items2 = lists[screen] ?? [];
|
|
442
|
+
if (key.escape) {
|
|
443
|
+
if (screen === "menu") props.onBack();
|
|
444
|
+
else if (screen === "controlActions") go("controls", chosen ?? 0);
|
|
445
|
+
else go(current?.fresh && screen !== "fresh" ? "fresh" : "menu");
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
if (!items2.length) return;
|
|
449
|
+
if (cursorStep(key)) setIndex((value) => wrapCursor(value, cursorStep(key), items2.length));
|
|
450
|
+
else if (key.return) items2[index]?.run();
|
|
451
|
+
else if (screen === "controls" && /^\d$/.test(input)) {
|
|
452
|
+
const next = `${digits}${input}`.replace(/^0+/, "");
|
|
453
|
+
const position = Number(next) - 1;
|
|
454
|
+
if (position >= 0 && position < items2.length) {
|
|
455
|
+
setDigits(next);
|
|
456
|
+
setIndex(position);
|
|
457
|
+
} else {
|
|
458
|
+
setDigits(input);
|
|
459
|
+
if (Number(input) >= 1 && Number(input) <= items2.length) setIndex(Number(input) - 1);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
});
|
|
463
|
+
const items = lists[screen] ?? [];
|
|
464
|
+
const sessionLine = current ? `Session ${current.sessionId} \xB7 ${current.mode}${current.fresh ? " \xB7 fresh (isolated \u2014 does not affect the Controller Session)" : ""}` : "No current session";
|
|
465
|
+
const titles = {
|
|
466
|
+
menu: "Web",
|
|
467
|
+
fresh: `${FRESH} \u2014 isolated \u2014 does not affect the Controller Session`,
|
|
468
|
+
device: "Device",
|
|
469
|
+
visibility: "Browser",
|
|
470
|
+
sessions: live.length ? "Live sessions" : "No live sessions",
|
|
471
|
+
controls: "Choose a control from the latest inspect",
|
|
472
|
+
controlActions: chosen !== void 0 && controls[chosen] ? `${chosen + 1}. [${controls[chosen].kind}] ${controls[chosen].label}` : "Control"
|
|
473
|
+
};
|
|
474
|
+
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", gap: 1, children: [
|
|
475
|
+
/* @__PURE__ */ jsx(Text, { dimColor: true, children: sessionLine }),
|
|
476
|
+
screen === "busy" && /* @__PURE__ */ jsx(Text, { children: busy }),
|
|
477
|
+
screen === "blocked" && /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
|
|
478
|
+
/* @__PURE__ */ jsxs(Text, { color: "yellow", children: [
|
|
479
|
+
"Log in first. The ",
|
|
480
|
+
props.environment,
|
|
481
|
+
" Controller Session is not authenticated, so no browser was opened."
|
|
482
|
+
] }),
|
|
483
|
+
/* @__PURE__ */ jsx(Text, { dimColor: true, children: "Enter to log in \xB7 Esc to return" })
|
|
484
|
+
] }),
|
|
485
|
+
screen === "text" && prompt && /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
|
|
486
|
+
/* @__PURE__ */ jsxs(Text, { children: [
|
|
487
|
+
prompt.title,
|
|
488
|
+
prompt.toggleSecret ? secret ? " (secret)" : "" : ""
|
|
489
|
+
] }),
|
|
490
|
+
/* @__PURE__ */ jsxs(Text, { children: [
|
|
491
|
+
secret ? "\u2022".repeat(typedLength) : typedVisible,
|
|
492
|
+
/* @__PURE__ */ jsx(Text, { inverse: true, children: " " })
|
|
493
|
+
] }),
|
|
494
|
+
/* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
|
|
495
|
+
"Enter to submit \xB7 Esc to cancel",
|
|
496
|
+
prompt.toggleSecret ? " \xB7 Tab to mark secret" : ""
|
|
497
|
+
] })
|
|
498
|
+
] }),
|
|
499
|
+
screen === "scroll" && /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
|
|
500
|
+
/* @__PURE__ */ jsxs(Text, { children: [
|
|
501
|
+
"Scroll ",
|
|
502
|
+
scrollReference ? `${(chosen ?? 0) + 1}. [${scrollReference.kind}] ${scrollReference.label}` : "the page"
|
|
503
|
+
] }),
|
|
504
|
+
/* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2191/\u2193 to scroll \xB7 PgUp/PgDn by page \xB7 \u2190/\u2192 sideways \xB7 Esc to return" })
|
|
505
|
+
] }),
|
|
506
|
+
titles[screen] !== void 0 && /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
|
|
507
|
+
/* @__PURE__ */ jsx(Text, { bold: true, children: titles[screen] }),
|
|
508
|
+
items.map((item, position) => /* @__PURE__ */ jsxs(Text, { color: position === index ? "cyan" : void 0, children: [
|
|
509
|
+
position === index ? "\u203A" : " ",
|
|
510
|
+
" ",
|
|
511
|
+
item.label
|
|
512
|
+
] }, `${position}-${item.label}`)),
|
|
513
|
+
/* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
|
|
514
|
+
"\u2191/\u2193 to move \xB7 Enter to choose",
|
|
515
|
+
screen === "controls" ? " \xB7 type a number" : "",
|
|
516
|
+
" \xB7 Esc to go back"
|
|
517
|
+
] })
|
|
518
|
+
] }),
|
|
519
|
+
lines.length > 0 && !["busy", "text", "controls", "controlActions", "sessions"].includes(screen) && /* @__PURE__ */ jsx(Box, { flexDirection: "column", children: lines.map((line, position) => /* @__PURE__ */ jsx(Text, { children: line }, position)) }),
|
|
520
|
+
error ? /* @__PURE__ */ jsx(Text, { color: "red", children: error }) : null
|
|
521
|
+
] });
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
// src/command-menu.tsx
|
|
525
|
+
import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
526
|
+
var COMMAND_GROUPS = [
|
|
527
|
+
{ label: "Settings", items: [
|
|
528
|
+
{ label: "Read", commands: ["settings read"], needs: "logged in", flow: "readSettings" },
|
|
529
|
+
{ label: "Edit and save", commands: ["settings save"], needs: "logged in", flow: "editSettings" }
|
|
530
|
+
] },
|
|
531
|
+
{ label: "Token", items: [
|
|
532
|
+
{ label: "Read", commands: ["token read"], needs: "logged in", flow: "readToken" },
|
|
533
|
+
{ label: "Replace", commands: ["token replace"], needs: "logged in", flow: "replaceToken" },
|
|
534
|
+
{ label: "Verify current", commands: ["token replace"], needs: "logged in", flow: "verifyToken" }
|
|
535
|
+
] },
|
|
536
|
+
{ label: "Pairs", items: [
|
|
537
|
+
{ label: "List", commands: ["pair list"], needs: "logged in", flow: "listPairs" },
|
|
538
|
+
{ label: "Assign", commands: ["pair assign"], needs: "logged in", flow: "assignPair" },
|
|
539
|
+
{ label: "Unpair", commands: ["pair unpair"], needs: "logged in", flow: "unpair" },
|
|
540
|
+
{ label: "Report", commands: ["pair report"], needs: "logged in", flow: "report" }
|
|
541
|
+
] },
|
|
542
|
+
{ label: "Candle Export", direct: true, items: [{ label: "Candle Export", commands: ["candle-export"], needs: "logged in", flow: "candleExport" }] },
|
|
543
|
+
{ label: "Zerodha", items: [
|
|
544
|
+
{ label: "Status", commands: ["zerodha status"], needs: "operator session", flow: "zerodhaStatus" },
|
|
545
|
+
{ label: "Observe attempt", commands: ["zerodha observe"], needs: "operator session", flow: "observeAttempt" },
|
|
546
|
+
{ label: "Start login and OTP", commands: ["zerodha login", "zerodha otp"], needs: "none", flow: "logIn" },
|
|
547
|
+
{ label: "Validate Zerodha Session", commands: ["validate zerodha-session"], needs: "operator session", flow: "validateZerodha" },
|
|
548
|
+
{ label: "Resume Zerodha Validation", commands: ["validate zerodha-session"], needs: "operator session", flow: "resumeValidation" }
|
|
549
|
+
] },
|
|
550
|
+
{ label: "Checks", items: [{ label: "Validate Operator Session", commands: ["validate operator-session"], needs: "none", flow: "validateOperatorSession" }] },
|
|
551
|
+
{ label: "Credential", items: [{ label: "Provision SMS credential", commands: ["credential provision"], needs: "none", flow: "provision" }] }
|
|
552
|
+
];
|
|
553
|
+
var SESSION_MENU = { login: "Log in", status: "Status", logout: "Log out" };
|
|
554
|
+
var ATTEMPT_ID = /^[a-zA-Z0-9_-]{1,200}$/;
|
|
555
|
+
function errorCode(cause) {
|
|
556
|
+
const code = cause?.code;
|
|
557
|
+
return typeof code === "string" ? code : "action_failed";
|
|
558
|
+
}
|
|
559
|
+
function isUncertain(cause) {
|
|
560
|
+
return cause?.uncertain === true;
|
|
561
|
+
}
|
|
562
|
+
function settingsLines(settings) {
|
|
563
|
+
return [
|
|
564
|
+
`Settings version: ${settings.settingsVersion}`,
|
|
565
|
+
`Market Protection: ${settings.marketProtection}%`,
|
|
566
|
+
`Entry Categories: ${settings.entryCategories.length ? settings.entryCategories.join(", ") : "none"}`
|
|
567
|
+
];
|
|
568
|
+
}
|
|
569
|
+
function tokenLines(metadata) {
|
|
570
|
+
return [
|
|
571
|
+
`Fingerprint: ${metadata.fingerprint ?? "none"}`,
|
|
572
|
+
`Version: ${metadata.version}`,
|
|
573
|
+
`Updated: ${metadata.updatedAt ?? "never"}`,
|
|
574
|
+
...metadata.writePending ? ["Write pending: yes"] : []
|
|
575
|
+
];
|
|
576
|
+
}
|
|
577
|
+
function pairLine(pair) {
|
|
578
|
+
return `${pair.pairId} \xB7 CE row ${pair.ceRow} ${pair.ceInstrument.tradingSymbol} \xB7 PE row ${pair.peRow} ${pair.peInstrument.tradingSymbol} \xB7 ${pair.pairLots} lots` + (pair.endReason ? ` \xB7 ended: ${pair.endReason}` : "");
|
|
579
|
+
}
|
|
580
|
+
function validationLines(result) {
|
|
581
|
+
return [
|
|
582
|
+
`Outcome: ${result.outcome}`,
|
|
583
|
+
`Reason: ${result.reason}`,
|
|
584
|
+
...result.attemptId ? [`Attempt ID: ${result.attemptId}`] : [],
|
|
585
|
+
...result.observedState ? [`Observed state: ${result.observedState}`] : [],
|
|
586
|
+
...result.message ? [`Message: ${result.message}`] : [],
|
|
587
|
+
...result.nextStep ? [`Next step: ${result.nextStep}`] : []
|
|
588
|
+
];
|
|
589
|
+
}
|
|
590
|
+
var sameSet = (left, right) => left.length === right.length && left.every((value) => right.includes(value));
|
|
591
|
+
function CommandMenu(props) {
|
|
592
|
+
const { group, actions, memory } = props;
|
|
593
|
+
const stack = useRef2(void 0);
|
|
594
|
+
const [, setRevision] = useState2(0);
|
|
595
|
+
const [cursor, setCursor] = useState2(0);
|
|
596
|
+
const [selection, setSelection] = useState2([]);
|
|
597
|
+
const [notice, setNotice] = useState2("");
|
|
598
|
+
const text = useRef2("");
|
|
599
|
+
const [textLength, setTextLength] = useState2(0);
|
|
600
|
+
const [visibleText, setVisibleText] = useState2("");
|
|
601
|
+
const setText = (value) => {
|
|
602
|
+
text.current = value;
|
|
603
|
+
setTextLength(value.length);
|
|
604
|
+
setVisibleText(value);
|
|
605
|
+
};
|
|
606
|
+
const root = { kind: "menu", title: group.label, items: () => group.items.map((item) => ({ label: item.label, run: () => start(item) })) };
|
|
607
|
+
const views = () => stack.current ?? (group.direct ? [] : [root]);
|
|
608
|
+
const view = views().at(-1);
|
|
609
|
+
function enter(next) {
|
|
610
|
+
stack.current = next;
|
|
611
|
+
setRevision((revision) => revision + 1);
|
|
612
|
+
setCursor(0);
|
|
613
|
+
setNotice("");
|
|
614
|
+
const top = next.at(-1);
|
|
615
|
+
setText(top?.kind === "input" ? top.initial ?? "" : "");
|
|
616
|
+
if (top?.kind === "multi") setSelection([...top.selected]);
|
|
617
|
+
}
|
|
618
|
+
const push = (next) => enter([...views(), next]);
|
|
619
|
+
const finish = (next) => enter(group.direct ? [next] : [root, next]);
|
|
620
|
+
function back() {
|
|
621
|
+
const current = views();
|
|
622
|
+
if (current.length <= 1) props.onBack();
|
|
623
|
+
else enter(current.slice(0, -1));
|
|
624
|
+
}
|
|
625
|
+
useEffect(() => {
|
|
626
|
+
if (group.direct) start(group.items[0]);
|
|
627
|
+
}, []);
|
|
628
|
+
const busy = (title) => finish({ kind: "busy", title });
|
|
629
|
+
const show = (title, lines, extra = {}) => finish({ kind: "result", title, lines, ...extra });
|
|
630
|
+
function logInFirst(lines = []) {
|
|
631
|
+
show("Log in first", [...lines, "This needs a usable Controller Session."], {
|
|
632
|
+
tone: "warning",
|
|
633
|
+
enter: { key: "Enter", label: "log in now", run: props.onLogIn }
|
|
634
|
+
});
|
|
635
|
+
}
|
|
636
|
+
function failed(title, cause, options = {}) {
|
|
637
|
+
const code = errorCode(cause);
|
|
638
|
+
if (isLoginRequired(code)) {
|
|
639
|
+
logInFirst([`Reason: ${code}`]);
|
|
640
|
+
return;
|
|
641
|
+
}
|
|
642
|
+
let message = cause instanceof Error ? cause.message : "The action failed.";
|
|
643
|
+
for (const secret of options.secrets ?? []) if (secret) message = message.replaceAll(secret, "[redacted]");
|
|
644
|
+
if (isUncertain(cause)) {
|
|
645
|
+
show(title, [options.uncertain ?? "Result uncertain; check before trying again.", "Nothing was resubmitted.", `Reason: ${code}`], { tone: "warning" });
|
|
646
|
+
return;
|
|
647
|
+
}
|
|
648
|
+
const next = options.keys?.[code];
|
|
649
|
+
show(title, [`Reason: ${code}`, message, ...next ? ["Nothing was retried."] : []], { tone: "error", ...next ? { keys: [next] } : {} });
|
|
650
|
+
}
|
|
651
|
+
async function guard(item, run) {
|
|
652
|
+
if (item.needs !== "none") {
|
|
653
|
+
busy("Checking the Controller Session\u2026");
|
|
654
|
+
try {
|
|
655
|
+
const report2 = await actions.status();
|
|
656
|
+
if (!sessionUsable(report2, item.needs)) {
|
|
657
|
+
logInFirst([`Operator Session: ${report2.operatorSession}`, `Zerodha: ${report2.zerodha}`]);
|
|
658
|
+
return;
|
|
659
|
+
}
|
|
660
|
+
} catch (cause) {
|
|
661
|
+
failed(item.label, cause);
|
|
662
|
+
return;
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
await run();
|
|
666
|
+
}
|
|
667
|
+
async function attempt(title, busyTitle, action, done, options = {}) {
|
|
668
|
+
busy(busyTitle);
|
|
669
|
+
try {
|
|
670
|
+
done(await action());
|
|
671
|
+
} catch (cause) {
|
|
672
|
+
failed(title, cause, options);
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
function start(item) {
|
|
676
|
+
const flows = {
|
|
677
|
+
readSettings: () => attempt("Settings", "Reading Settings\u2026", () => actions.readSettings(), (settings) => show("Settings", settingsLines(settings))),
|
|
678
|
+
editSettings,
|
|
679
|
+
readToken: () => attempt("Token", "Reading token metadata\u2026", () => actions.readToken(), (metadata) => show("Token metadata", tokenLines(metadata))),
|
|
680
|
+
replaceToken,
|
|
681
|
+
verifyToken,
|
|
682
|
+
listPairs,
|
|
683
|
+
assignPair,
|
|
684
|
+
unpair: () => pickPair("Unpair", false, unpair),
|
|
685
|
+
report: () => pickPair("Report", true, report),
|
|
686
|
+
candleExport,
|
|
687
|
+
zerodhaStatus: () => attempt("Zerodha", "Checking the Zerodha Session\u2026", () => actions.zerodha("status"), (result) => show("Zerodha Session", ["Zerodha Session: present", ...result.observedOperator ? [`Operator: ${result.observedOperator}`] : []])),
|
|
688
|
+
observeAttempt,
|
|
689
|
+
logIn: props.onLogIn,
|
|
690
|
+
validateZerodha: () => startValidation(),
|
|
691
|
+
resumeValidation,
|
|
692
|
+
validateOperatorSession: () => attempt(
|
|
693
|
+
"Validate Operator Session",
|
|
694
|
+
"Validating the Operator Session in temporary profiles\u2026",
|
|
695
|
+
() => actions.validateOperatorSession(),
|
|
696
|
+
(result) => show("Validate Operator Session", [
|
|
697
|
+
`Outcome: ${result.outcome}`,
|
|
698
|
+
`Reason: ${result.reason}`,
|
|
699
|
+
`Expected Operator: ${result.expectedOperator}`,
|
|
700
|
+
...result.observedOperator ? [`Observed Operator: ${result.observedOperator}`] : [],
|
|
701
|
+
`Temporary profiles cleaned up: ${result.cleanup.outcome}`
|
|
702
|
+
], result.outcome === "pass" ? {} : { tone: "warning" })
|
|
703
|
+
),
|
|
704
|
+
provision: props.onProvision
|
|
705
|
+
};
|
|
706
|
+
void guard(item, flows[item.flow]);
|
|
707
|
+
}
|
|
708
|
+
async function editSettings() {
|
|
709
|
+
busy("Reading Settings\u2026");
|
|
710
|
+
let current;
|
|
711
|
+
try {
|
|
712
|
+
current = await actions.readSettings();
|
|
713
|
+
} catch (cause) {
|
|
714
|
+
failed("Settings", cause);
|
|
715
|
+
return;
|
|
716
|
+
}
|
|
717
|
+
const draft = {};
|
|
718
|
+
const credentials = {};
|
|
719
|
+
const credentialFields = [
|
|
720
|
+
{ key: "userId", label: "User ID", mask: false },
|
|
721
|
+
{ key: "password", label: "Password", mask: true },
|
|
722
|
+
{ key: "apiKey", label: "API key", mask: false },
|
|
723
|
+
{ key: "apiSecret", label: "API secret", mask: true }
|
|
724
|
+
];
|
|
725
|
+
const credentialsForm = { kind: "menu", title: "Replace Zerodha Credentials (all four fields)", items: () => [
|
|
726
|
+
...credentialFields.map((field) => ({
|
|
727
|
+
label: `${field.label}: ${credentials[field.key] ? "entered" : "\u2014"}`,
|
|
728
|
+
run: () => push({ kind: "input", title: field.label, mask: field.mask, submit: (value) => {
|
|
729
|
+
if (!value) return `Enter the ${field.label}.`;
|
|
730
|
+
credentials[field.key] = value;
|
|
731
|
+
back();
|
|
732
|
+
} })
|
|
733
|
+
})),
|
|
734
|
+
{ label: "Use these credentials", run: () => {
|
|
735
|
+
if (credentialFields.some((field) => !credentials[field.key])) {
|
|
736
|
+
setNotice("Enter all four Zerodha Credentials fields; a partial replacement is rejected.");
|
|
737
|
+
return;
|
|
738
|
+
}
|
|
739
|
+
draft.credentials = { ...credentials };
|
|
740
|
+
back();
|
|
741
|
+
} },
|
|
742
|
+
{ label: "Keep the saved credentials", run: () => {
|
|
743
|
+
delete draft.credentials;
|
|
744
|
+
for (const field of credentialFields) delete credentials[field.key];
|
|
745
|
+
back();
|
|
746
|
+
} }
|
|
747
|
+
] };
|
|
748
|
+
const form = { kind: "menu", title: `Edit Settings (version ${current.settingsVersion})`, items: () => [
|
|
749
|
+
{
|
|
750
|
+
label: `Market Protection: ${draft.marketProtection ?? current.marketProtection}%${draft.marketProtection === void 0 ? "" : " (changed)"}`,
|
|
751
|
+
run: () => push({ kind: "input", title: "Market Protection (2\u201390 %)", initial: String(draft.marketProtection ?? current.marketProtection), submit: (value) => {
|
|
752
|
+
const next = Number(value);
|
|
753
|
+
if (!/^\d+$/.test(value) || !isOperatorMarketProtection(next)) return "Market Protection must be a whole number from 2 to 90.";
|
|
754
|
+
if (next === current.marketProtection) delete draft.marketProtection;
|
|
755
|
+
else draft.marketProtection = next;
|
|
756
|
+
back();
|
|
757
|
+
} })
|
|
758
|
+
},
|
|
759
|
+
{
|
|
760
|
+
label: `Entry Categories: ${(draft.entryCategories ?? current.entryCategories).join(", ") || "none"}${draft.entryCategories ? " (changed)" : ""}`,
|
|
761
|
+
run: () => push({
|
|
762
|
+
kind: "multi",
|
|
763
|
+
title: "Entry Categories",
|
|
764
|
+
options: ENTRY_CATEGORIES,
|
|
765
|
+
selected: draft.entryCategories ?? current.entryCategories,
|
|
766
|
+
submit: (selected) => {
|
|
767
|
+
const next = ENTRY_CATEGORIES.filter((category) => selected.includes(category));
|
|
768
|
+
if (sameSet(next, current.entryCategories)) delete draft.entryCategories;
|
|
769
|
+
else draft.entryCategories = next;
|
|
770
|
+
back();
|
|
771
|
+
}
|
|
772
|
+
})
|
|
773
|
+
},
|
|
774
|
+
{ label: `Replace Zerodha Credentials: ${draft.credentials ? "all four fields entered" : "no"}`, run: () => push(credentialsForm) },
|
|
775
|
+
{ label: "Save", run: () => {
|
|
776
|
+
if (Object.keys(draft).length === 0) {
|
|
777
|
+
setNotice("Nothing changed.");
|
|
778
|
+
return;
|
|
779
|
+
}
|
|
780
|
+
const patch = { expectedSettingsVersion: current.settingsVersion, ...draft };
|
|
781
|
+
const secrets = [patch.credentials?.password ?? "", patch.credentials?.apiSecret ?? ""];
|
|
782
|
+
void attempt("Settings", "Saving Settings\u2026", () => actions.saveSettings(patch), (result) => show(result.outcome === "saved" ? "Settings saved" : "Settings unchanged", settingsLines(result.settings)), {
|
|
783
|
+
uncertain: "Result uncertain; read Settings before trying again.",
|
|
784
|
+
secrets,
|
|
785
|
+
keys: { settings_conflict: { key: "r", label: "reload Settings and edit again", run: () => void editSettings() } }
|
|
786
|
+
});
|
|
787
|
+
} }
|
|
788
|
+
] };
|
|
789
|
+
enter(group.direct ? [form] : [root, form]);
|
|
790
|
+
}
|
|
791
|
+
async function readTokenThen(next) {
|
|
792
|
+
busy("Reading token metadata\u2026");
|
|
793
|
+
try {
|
|
794
|
+
next(await actions.readToken());
|
|
795
|
+
} catch (cause) {
|
|
796
|
+
failed("Token", cause);
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
function tokenResult(title, request) {
|
|
800
|
+
const secrets = [request.accessToken ?? ""];
|
|
801
|
+
void attempt(title, "Submitting to Gate\u2026", () => actions.replaceToken(request), (result) => show(title, [
|
|
802
|
+
`Outcome: ${result.outcome}`,
|
|
803
|
+
`Sharing: ${result.sharing}`,
|
|
804
|
+
...tokenLines(result.metadata)
|
|
805
|
+
]), {
|
|
806
|
+
uncertain: "Result uncertain; read token metadata before trying again.",
|
|
807
|
+
secrets,
|
|
808
|
+
...request.continueReplacement ? {} : { keys: { active_trading_confirmation: { key: "c", label: "continue despite active trading (confirm next)", run: () => push({
|
|
809
|
+
kind: "confirm",
|
|
810
|
+
title: "Another device has active trading. Continue the replacement? y/N",
|
|
811
|
+
yes: () => tokenResult(title, { ...request, continueReplacement: true })
|
|
812
|
+
}) } } }
|
|
813
|
+
});
|
|
814
|
+
}
|
|
815
|
+
function replaceToken() {
|
|
816
|
+
return readTokenThen((metadata) => finish({ kind: "input", title: "New Zerodha access token", mask: true, submit: (token) => {
|
|
817
|
+
if (!token.trim()) return "Enter the access token.";
|
|
818
|
+
push({
|
|
819
|
+
kind: "confirm",
|
|
820
|
+
title: `Replace the token whose fingerprint is ${metadata.fingerprint ?? "none"} (version ${metadata.version})? y/N`,
|
|
821
|
+
yes: () => tokenResult("Token replace", { accessToken: token, expected: metadata })
|
|
822
|
+
});
|
|
823
|
+
} }));
|
|
824
|
+
}
|
|
825
|
+
function verifyToken() {
|
|
826
|
+
return readTokenThen((metadata) => finish({
|
|
827
|
+
kind: "confirm",
|
|
828
|
+
title: `Verify the saved token whose fingerprint is ${metadata.fingerprint ?? "none"} with Zerodha? y/N`,
|
|
829
|
+
yes: () => tokenResult("Token verify", { expected: metadata, verifyCurrent: true })
|
|
830
|
+
}));
|
|
831
|
+
}
|
|
832
|
+
const pairUncertain = "Result uncertain; check Pair list before trying again.";
|
|
833
|
+
function runPair(title, action, done) {
|
|
834
|
+
return attempt(title, "Sending to the Broker\u2026", () => actions.pair(action), done, { uncertain: pairUncertain });
|
|
835
|
+
}
|
|
836
|
+
function listPairs() {
|
|
837
|
+
return runPair("Pairs", { action: "list" }, ({ pairs }) => show("Pairs", [
|
|
838
|
+
"Active pairs",
|
|
839
|
+
...pairs.active.length ? pairs.active.map((pair) => ` ${pairLine(pair)}`) : [" none"],
|
|
840
|
+
"Past pairs",
|
|
841
|
+
...pairs.past.length ? pairs.past.map((pair) => ` ${pairLine(pair)}`) : [" none"]
|
|
842
|
+
]));
|
|
843
|
+
}
|
|
844
|
+
function assignPair() {
|
|
845
|
+
const row = (value) => /^\d{1,3}$/.test(value) ? void 0 : "Enter a Market Watch row number.";
|
|
846
|
+
finish({ kind: "input", title: "CE row (Market Watch row number)", submit: (ce) => row(ce) ?? push({
|
|
847
|
+
kind: "input",
|
|
848
|
+
title: "PE row (Market Watch row number)",
|
|
849
|
+
submit: (pe) => row(pe) ?? push({ kind: "input", title: "Pair Lots", submit: (lots) => {
|
|
850
|
+
if (!/^-?\d{1,6}$/.test(lots)) return "Enter the Pair Lots as a whole number.";
|
|
851
|
+
void runPair("Pair assign", { action: "assign", ceRow: Number(ce), peRow: Number(pe), pairLots: Number(lots) }, ({ pair }) => show("Pair assigned", [pairLine(pair)]));
|
|
852
|
+
} })
|
|
853
|
+
}) });
|
|
854
|
+
}
|
|
855
|
+
async function pickPair(title, includePast, next) {
|
|
856
|
+
await runPair(title, { action: "list" }, ({ pairs }) => {
|
|
857
|
+
const choices = includePast ? [...pairs.active, ...pairs.past] : pairs.active;
|
|
858
|
+
if (!choices.length) {
|
|
859
|
+
show(title, [includePast ? "No pairs yet." : "No active pairs."]);
|
|
860
|
+
return;
|
|
861
|
+
}
|
|
862
|
+
finish({ kind: "menu", title: `${title}: choose a pair`, items: () => choices.map((pair) => ({ label: pairLine(pair), run: () => next(pair) })) });
|
|
863
|
+
});
|
|
864
|
+
}
|
|
865
|
+
function unpair(pair) {
|
|
866
|
+
push({
|
|
867
|
+
kind: "confirm",
|
|
868
|
+
title: `Unpair ${pair.pairId}? Any paper position closes at its latest complete close. y/N`,
|
|
869
|
+
yes: () => void runPair("Pair unpair", { action: "unpair", pairId: pair.pairId }, ({ unpair: result }) => show("Pair unpair", [
|
|
870
|
+
`Outcome: ${result.outcome}`,
|
|
871
|
+
`Pair: ${pair.pairId}`,
|
|
872
|
+
...result.exit ? ["A paper position was closed."] : []
|
|
873
|
+
]))
|
|
874
|
+
});
|
|
875
|
+
}
|
|
876
|
+
function report(pair) {
|
|
877
|
+
const save = (output) => void runPair(
|
|
878
|
+
"Pair report",
|
|
879
|
+
{ action: "report", pairId: pair.pairId, publish: false, ...output ? { output } : {} },
|
|
880
|
+
({ report: result }) => result && !result.published && show("Pair report saved", [
|
|
881
|
+
`File: ${result.path}`,
|
|
882
|
+
`Bytes: ${result.bytes}`,
|
|
883
|
+
...result.decisionCount === null ? [] : [`Decisions: ${result.decisionCount}`]
|
|
884
|
+
])
|
|
885
|
+
);
|
|
886
|
+
push({ kind: "menu", title: `Report for ${pair.pairId}`, items: () => [
|
|
887
|
+
{ label: "Publish (Hosted URL)", run: () => void runPair(
|
|
888
|
+
"Pair report",
|
|
889
|
+
{ action: "report", pairId: pair.pairId, publish: true },
|
|
890
|
+
({ report: result }) => result?.published && show("Pair report published", [`Hosted URL: ${result.url}`, `File: ${result.filename}`])
|
|
891
|
+
) },
|
|
892
|
+
{ label: "Save to a file", run: () => push({ kind: "input", title: "Path (blank: the server filename here, never overwritten)", submit: (value) => {
|
|
893
|
+
if (!value) return void save(void 0);
|
|
894
|
+
const output = path.resolve(value);
|
|
895
|
+
void access(output).then(
|
|
896
|
+
() => push({ kind: "confirm", title: `${output} exists and will be overwritten. Continue? y/N`, yes: () => save(value) }),
|
|
897
|
+
() => save(value)
|
|
898
|
+
);
|
|
899
|
+
} }) }
|
|
900
|
+
] });
|
|
901
|
+
}
|
|
902
|
+
function candleExport() {
|
|
903
|
+
finish({ kind: "input", title: "Instrument (EXCHANGE:SYMBOL)", submit: (value) => {
|
|
904
|
+
const instrument = parseCandleExportInstrument(value);
|
|
905
|
+
if (!instrument) return "Enter EXCHANGE:SYMBOL, for example NSE:INFY.";
|
|
906
|
+
push({ kind: "input", title: "Date (YYYY-MM-DD; blank: the latest completed trading day)", submit: (date) => {
|
|
907
|
+
if (date && !isCandleExportDate(date)) return "Enter a date as YYYY-MM-DD, or leave it blank.";
|
|
908
|
+
push({ kind: "input", title: "Save a local copy to (blank: link only; never overwrites)", submit: (output) => {
|
|
909
|
+
void attempt(
|
|
910
|
+
"Candle Export",
|
|
911
|
+
"Publishing the Candle Export\u2026",
|
|
912
|
+
() => actions.exportCandles({ ...instrument, ...date ? { date } : {} }, output || void 0),
|
|
913
|
+
({ candleExport: result, download }) => show("Candle Export", [
|
|
914
|
+
`Candle Export Link: ${result.url}`,
|
|
915
|
+
`Instrument: ${result.exchange}:${result.tradingSymbol}`,
|
|
916
|
+
`Session date: ${result.date}`,
|
|
917
|
+
`LMT candles: ${result.lmt.availability}${result.lmt.reason ? ` (${result.lmt.reason})` : ""}`,
|
|
918
|
+
`Reference candles: ${result.reference.availability}${result.reference.reason ? ` (${result.reference.reason})` : ""}`,
|
|
919
|
+
`Observed minutes: ${result.observedMinuteCount}`,
|
|
920
|
+
...download ? [`Saved: ${download.path}`] : []
|
|
921
|
+
])
|
|
922
|
+
);
|
|
923
|
+
} });
|
|
924
|
+
} });
|
|
925
|
+
} });
|
|
926
|
+
}
|
|
927
|
+
function attemptInput(next) {
|
|
928
|
+
finish({ kind: "input", title: "Zerodha attempt ID", ...memory.attemptId ? { initial: memory.attemptId } : {}, submit: (value) => {
|
|
929
|
+
if (!ATTEMPT_ID.test(value)) return "Enter the attempt ID from Zerodha login or validation.";
|
|
930
|
+
memory.attemptId = value;
|
|
931
|
+
next(value);
|
|
932
|
+
} });
|
|
933
|
+
}
|
|
934
|
+
function observeAttempt() {
|
|
935
|
+
attemptInput((attemptId) => void attempt("Zerodha attempt", "Observing the attempt\u2026", () => actions.zerodha("observe", attemptId), (result) => show("Zerodha attempt", [
|
|
936
|
+
`Attempt ID: ${attemptId}`,
|
|
937
|
+
`Status: ${result.status}`,
|
|
938
|
+
...result.observedState ? [`Observed state: ${result.observedState}`] : [],
|
|
939
|
+
...result.message ? [`Message: ${result.message}`] : []
|
|
940
|
+
])));
|
|
941
|
+
}
|
|
942
|
+
async function startValidation() {
|
|
943
|
+
busy("Checking the Controller Session\u2026");
|
|
944
|
+
let report2;
|
|
945
|
+
try {
|
|
946
|
+
report2 = await actions.status();
|
|
947
|
+
} catch (cause) {
|
|
948
|
+
failed("Validate Zerodha Session", cause);
|
|
949
|
+
return;
|
|
950
|
+
}
|
|
951
|
+
if (report2.zerodha !== "authenticated") return validateZerodha();
|
|
952
|
+
finish({
|
|
953
|
+
kind: "confirm",
|
|
954
|
+
yes: () => void validateZerodha(),
|
|
955
|
+
title: "Zerodha is already logged in. Validation starts a new Zerodha login and asks for a fresh OTP. Continue? y/N"
|
|
956
|
+
});
|
|
957
|
+
}
|
|
958
|
+
function validateZerodha(attemptId, otp, rejected) {
|
|
959
|
+
return attempt("Validate Zerodha Session", "Validating the Zerodha Session\u2026", () => actions.validateZerodhaSession(attemptId, otp), (result) => {
|
|
960
|
+
if (result.attemptId) memory.attemptId = result.attemptId;
|
|
961
|
+
const waiting = result.attemptId && (result.reason === "awaiting_otp" || result.reason === "otp_rejected");
|
|
962
|
+
if (!waiting) {
|
|
963
|
+
show("Validate Zerodha Session", validationLines(result), result.outcome === "pass" ? {} : { tone: "warning" });
|
|
964
|
+
return;
|
|
965
|
+
}
|
|
966
|
+
const retry = result.reason === "otp_rejected" ? result.message ?? "Zerodha rejected the OTP." : rejected;
|
|
967
|
+
finish({ kind: "input", title: "Zerodha OTP", mask: true, ...retry ? { hint: retry } : {}, submit: (value) => {
|
|
968
|
+
if (!/^\d{6}$/.test(value)) return "Enter the 6-digit Zerodha OTP.";
|
|
969
|
+
void validateZerodha(result.attemptId, value);
|
|
970
|
+
} });
|
|
971
|
+
}, { secrets: [otp ?? ""] });
|
|
972
|
+
}
|
|
973
|
+
function resumeValidation() {
|
|
974
|
+
attemptInput((attemptId) => push({ kind: "menu", title: `Resume ${attemptId}`, items: () => [
|
|
975
|
+
{ label: "Submit the Zerodha OTP", run: () => push({ kind: "input", title: "Zerodha OTP", mask: true, submit: (otp) => {
|
|
976
|
+
if (!/^\d{6}$/.test(otp)) return "Enter the 6-digit Zerodha OTP.";
|
|
977
|
+
void validateZerodha(attemptId, otp);
|
|
978
|
+
} }) },
|
|
979
|
+
{ label: "Observe only", run: () => void validateZerodha(attemptId) }
|
|
980
|
+
] }));
|
|
981
|
+
}
|
|
982
|
+
useInput2((input, key) => {
|
|
983
|
+
if (!view || view.kind === "busy") return;
|
|
984
|
+
if (key.escape) {
|
|
985
|
+
back();
|
|
986
|
+
return;
|
|
987
|
+
}
|
|
988
|
+
if (view.kind === "menu") {
|
|
989
|
+
const items = view.items();
|
|
990
|
+
if (cursorStep(key)) setCursor((index) => wrapCursor(index, cursorStep(key), items.length));
|
|
991
|
+
else if (key.return) {
|
|
992
|
+
setNotice("");
|
|
993
|
+
items[cursor]?.run();
|
|
994
|
+
}
|
|
995
|
+
} else if (view.kind === "input") {
|
|
996
|
+
if (key.return) {
|
|
997
|
+
const value = text.current.trim();
|
|
998
|
+
if (!view.mask) view.initial = value;
|
|
999
|
+
const problem = view.submit(value);
|
|
1000
|
+
if (typeof problem === "string") setNotice(problem);
|
|
1001
|
+
} else if (key.backspace || key.delete) setText(text.current.slice(0, -1));
|
|
1002
|
+
else if (input && !key.ctrl && !key.meta) setText(text.current + input.replace(/[\r\n]/g, ""));
|
|
1003
|
+
} else if (view.kind === "multi") {
|
|
1004
|
+
if (cursorStep(key)) setCursor((index) => wrapCursor(index, cursorStep(key), view.options.length));
|
|
1005
|
+
else if (input === " ") {
|
|
1006
|
+
const option = view.options[cursor];
|
|
1007
|
+
setSelection((current) => current.includes(option) ? current.filter((value) => value !== option) : [...current, option]);
|
|
1008
|
+
} else if (key.return) view.submit(selection);
|
|
1009
|
+
} else if (view.kind === "confirm") {
|
|
1010
|
+
if (input === "y" || input === "Y") view.yes();
|
|
1011
|
+
else if (input || key.return) back();
|
|
1012
|
+
} else if (view.kind === "result") {
|
|
1013
|
+
if (key.return) {
|
|
1014
|
+
if (view.enter) view.enter.run();
|
|
1015
|
+
else back();
|
|
1016
|
+
} else {
|
|
1017
|
+
view.keys?.find((candidate) => candidate.key === input)?.run();
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
});
|
|
1021
|
+
if (!view) return /* @__PURE__ */ jsxs2(Text2, { children: [
|
|
1022
|
+
"Opening ",
|
|
1023
|
+
group.label,
|
|
1024
|
+
"\u2026"
|
|
1025
|
+
] });
|
|
1026
|
+
return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", children: [
|
|
1027
|
+
/* @__PURE__ */ jsx2(Text2, { bold: true, children: view.title }),
|
|
1028
|
+
view.kind === "menu" && view.items().map((item, index) => /* @__PURE__ */ jsxs2(Text2, { color: index === cursor ? "cyan" : void 0, children: [
|
|
1029
|
+
index === cursor ? "\u203A" : " ",
|
|
1030
|
+
" ",
|
|
1031
|
+
item.label
|
|
1032
|
+
] }, `${index}-${item.label}`)),
|
|
1033
|
+
view.kind === "input" && view.hint && /* @__PURE__ */ jsx2(Text2, { color: "yellow", children: view.hint }),
|
|
1034
|
+
view.kind === "input" && /* @__PURE__ */ jsxs2(Text2, { children: [
|
|
1035
|
+
view.mask ? "\u2022".repeat(textLength) : visibleText,
|
|
1036
|
+
/* @__PURE__ */ jsx2(Text2, { inverse: true, children: " " })
|
|
1037
|
+
] }),
|
|
1038
|
+
view.kind === "multi" && view.options.map((option, index) => /* @__PURE__ */ jsxs2(Text2, { color: index === cursor ? "cyan" : void 0, children: [
|
|
1039
|
+
index === cursor ? "\u203A" : " ",
|
|
1040
|
+
" [",
|
|
1041
|
+
selection.includes(option) ? "x" : " ",
|
|
1042
|
+
"] ",
|
|
1043
|
+
option
|
|
1044
|
+
] }, option)),
|
|
1045
|
+
view.kind === "result" && view.lines.map((line, index) => /* @__PURE__ */ jsx2(Text2, { color: view.tone === "error" ? "red" : view.tone === "warning" ? "yellow" : void 0, children: line }, index)),
|
|
1046
|
+
notice ? /* @__PURE__ */ jsx2(Text2, { color: "red", children: notice }) : null,
|
|
1047
|
+
/* @__PURE__ */ jsx2(Text2, { dimColor: true, children: view.kind === "menu" ? "\u2191/\u2193 to move \xB7 Enter to choose \xB7 Esc to go back" : view.kind === "input" ? "Enter to continue \xB7 Esc to go back" : view.kind === "multi" ? "\u2191/\u2193 to move \xB7 Space to toggle \xB7 Enter to keep \xB7 Esc to go back" : view.kind === "confirm" ? "y to confirm \xB7 any other key to cancel" : view.kind === "busy" ? "" : [
|
|
1048
|
+
view.enter ? `Enter to ${view.enter.label}` : "Enter or Esc to return",
|
|
1049
|
+
...(view.keys ?? []).map((candidate) => `${candidate.key} to ${candidate.label}`),
|
|
1050
|
+
...view.enter ? ["Esc to go back"] : []
|
|
1051
|
+
].join(" \xB7 ") })
|
|
1052
|
+
] });
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
// src/controller-app.tsx
|
|
1056
|
+
import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
1057
|
+
function loginStateOf(report) {
|
|
1058
|
+
if (report.smsCredential === "missing") return "SMS credential missing";
|
|
1059
|
+
if (report.operatorSession !== "valid") return "needs login";
|
|
1060
|
+
return report.zerodha === "authenticated" ? "logged in" : "needs Zerodha login";
|
|
1061
|
+
}
|
|
1062
|
+
function formatStatus(report) {
|
|
1063
|
+
return [
|
|
1064
|
+
`Environment: ${report.environment}`,
|
|
1065
|
+
`Origin: ${report.origin}`,
|
|
1066
|
+
`Session: ${report.session === "controller" ? "Controller Session" : `profile ${report.profile}`}`,
|
|
1067
|
+
`SMS credential: ${report.smsCredential}`,
|
|
1068
|
+
`Operator Session: ${report.operatorSession}`,
|
|
1069
|
+
`Zerodha: ${report.zerodha}`,
|
|
1070
|
+
...report.nextStep ? [`Next step: ${report.nextStep}`] : [],
|
|
1071
|
+
...report.error ? [`Error: ${report.error.message}`] : []
|
|
1072
|
+
].join("\n");
|
|
1073
|
+
}
|
|
1074
|
+
function ControllerApp(props) {
|
|
1075
|
+
const { environments, preferences, actionsFor } = props;
|
|
1076
|
+
const { exit } = useApp();
|
|
1077
|
+
const [screen, setScreen] = useState3(props.initialEnvironment ? "menu" : "environment");
|
|
1078
|
+
const [environment, setEnvironment] = useState3(props.initialEnvironment);
|
|
1079
|
+
const [environmentIndex, setEnvironmentIndex] = useState3(void 0);
|
|
1080
|
+
const [menuIndex, setMenuIndex] = useState3(0);
|
|
1081
|
+
const [report, setReport] = useState3(void 0);
|
|
1082
|
+
const [error, setError] = useState3("");
|
|
1083
|
+
const [notice, setNotice] = useState3("");
|
|
1084
|
+
const [busy, setBusy] = useState3("");
|
|
1085
|
+
const otp = useRef3("");
|
|
1086
|
+
const [otpLength, setOtpLength] = useState3(0);
|
|
1087
|
+
const setOtp = (update) => {
|
|
1088
|
+
otp.current = typeof update === "string" ? update : update(otp.current);
|
|
1089
|
+
setOtpLength(otp.current.length);
|
|
1090
|
+
};
|
|
1091
|
+
const attemptId = useRef3(void 0);
|
|
1092
|
+
const statusRequest = useRef3(0);
|
|
1093
|
+
const [commandGroup, setCommandGroup] = useState3(void 0);
|
|
1094
|
+
const commandMemory = useRef3({});
|
|
1095
|
+
useEffect2(() => {
|
|
1096
|
+
if (screen !== "environment" || environmentIndex !== void 0) return;
|
|
1097
|
+
void preferences.readEnvironment().then((remembered) => {
|
|
1098
|
+
const index = environments.findIndex((option) => option.environment === (remembered ?? "local"));
|
|
1099
|
+
setEnvironmentIndex(Math.max(0, index));
|
|
1100
|
+
});
|
|
1101
|
+
}, [screen, environmentIndex]);
|
|
1102
|
+
const selected = environments.find((option) => option.environment === environment);
|
|
1103
|
+
const webActions = useMemo(() => environment && props.webActionsFor ? props.webActionsFor(environment) : void 0, [environment]);
|
|
1104
|
+
async function refreshStatus() {
|
|
1105
|
+
if (!environment) return void 0;
|
|
1106
|
+
const request = ++statusRequest.current;
|
|
1107
|
+
try {
|
|
1108
|
+
const next = await actionsFor(environment).status();
|
|
1109
|
+
if (request === statusRequest.current) setReport(next);
|
|
1110
|
+
return next;
|
|
1111
|
+
} catch (cause) {
|
|
1112
|
+
if (request === statusRequest.current) setError(cause instanceof Error ? cause.message : "Could not check the session status.");
|
|
1113
|
+
return void 0;
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
useEffect2(() => {
|
|
1117
|
+
if (screen === "menu") void refreshStatus();
|
|
1118
|
+
}, [screen, environment]);
|
|
1119
|
+
function showBusy(message) {
|
|
1120
|
+
setBusy(message);
|
|
1121
|
+
setScreen("busy");
|
|
1122
|
+
}
|
|
1123
|
+
function backToMenu(outcome) {
|
|
1124
|
+
setOtp("");
|
|
1125
|
+
setNotice(outcome.notice ?? "");
|
|
1126
|
+
setError(outcome.error ?? "");
|
|
1127
|
+
setScreen("menu");
|
|
1128
|
+
}
|
|
1129
|
+
function failed(cause, fallback) {
|
|
1130
|
+
backToMenu({ error: cause instanceof Error ? cause.message : fallback });
|
|
1131
|
+
}
|
|
1132
|
+
async function logIn() {
|
|
1133
|
+
if (!environment) return;
|
|
1134
|
+
const actions = actionsFor(environment);
|
|
1135
|
+
setNotice("");
|
|
1136
|
+
setError("");
|
|
1137
|
+
showBusy("Checking the Controller Session\u2026");
|
|
1138
|
+
try {
|
|
1139
|
+
const current = await actions.status();
|
|
1140
|
+
setReport(current);
|
|
1141
|
+
if (current.smsCredential === "missing") {
|
|
1142
|
+
setScreen("credentialMissing");
|
|
1143
|
+
return;
|
|
1144
|
+
}
|
|
1145
|
+
if (loginStateOf(current) === "logged in") {
|
|
1146
|
+
backToMenu({ notice: "Already logged in." });
|
|
1147
|
+
return;
|
|
1148
|
+
}
|
|
1149
|
+
if (current.operatorSession !== "valid") {
|
|
1150
|
+
showBusy("Unlocking the Operator from SMS\u2026");
|
|
1151
|
+
await actions.unlockOperator();
|
|
1152
|
+
}
|
|
1153
|
+
showBusy("Starting Login with Zerodha\u2026");
|
|
1154
|
+
const attempt = await actions.startZerodhaLogin();
|
|
1155
|
+
if (attempt.status === "succeeded") {
|
|
1156
|
+
await finishLogin();
|
|
1157
|
+
return;
|
|
1158
|
+
}
|
|
1159
|
+
attemptId.current = attempt.attemptId;
|
|
1160
|
+
setOtp("");
|
|
1161
|
+
setScreen("otp");
|
|
1162
|
+
} catch (cause) {
|
|
1163
|
+
failed(cause, "Log in failed.");
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
async function submitOtp() {
|
|
1167
|
+
if (!environment) return;
|
|
1168
|
+
if (!/^\d{6}$/.test(otp.current)) {
|
|
1169
|
+
setError("Enter the 6-digit Zerodha OTP.");
|
|
1170
|
+
return;
|
|
1171
|
+
}
|
|
1172
|
+
if (!attemptId.current) {
|
|
1173
|
+
backToMenu({ error: "The Zerodha login attempt is no longer active. Choose Log in again." });
|
|
1174
|
+
return;
|
|
1175
|
+
}
|
|
1176
|
+
const code = otp.current;
|
|
1177
|
+
setOtp("");
|
|
1178
|
+
setError("");
|
|
1179
|
+
showBusy("Verifying the Zerodha OTP\u2026");
|
|
1180
|
+
try {
|
|
1181
|
+
const result = await actionsFor(environment).submitZerodhaOtp(attemptId.current, code);
|
|
1182
|
+
if (result.status === "succeeded") {
|
|
1183
|
+
await finishLogin();
|
|
1184
|
+
return;
|
|
1185
|
+
}
|
|
1186
|
+
setError(result.message ?? "Zerodha did not accept the OTP. Enter it again.");
|
|
1187
|
+
setScreen("otp");
|
|
1188
|
+
} catch (cause) {
|
|
1189
|
+
const message = cause instanceof Error ? cause.message.replaceAll(code, "[redacted]") : "Zerodha login failed.";
|
|
1190
|
+
backToMenu({ error: message });
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
async function finishLogin() {
|
|
1194
|
+
attemptId.current = void 0;
|
|
1195
|
+
backToMenu({ notice: "Logged in." });
|
|
1196
|
+
await refreshStatus();
|
|
1197
|
+
}
|
|
1198
|
+
async function showStatus() {
|
|
1199
|
+
setNotice("");
|
|
1200
|
+
setError("");
|
|
1201
|
+
showBusy("Checking the Controller Session\u2026");
|
|
1202
|
+
const current = await refreshStatus();
|
|
1203
|
+
if (current) setScreen("status");
|
|
1204
|
+
else backToMenu({ error: "Could not check the session status." });
|
|
1205
|
+
}
|
|
1206
|
+
async function logOut() {
|
|
1207
|
+
if (!environment) return;
|
|
1208
|
+
showBusy("Logging out\u2026");
|
|
1209
|
+
try {
|
|
1210
|
+
await actionsFor(environment).logout();
|
|
1211
|
+
backToMenu({ notice: "Logged out." });
|
|
1212
|
+
} catch (cause) {
|
|
1213
|
+
failed(cause, "Could not log out.");
|
|
1214
|
+
}
|
|
1215
|
+
await refreshStatus();
|
|
1216
|
+
}
|
|
1217
|
+
const loginState = report ? loginStateOf(report) : void 0;
|
|
1218
|
+
const menu = [
|
|
1219
|
+
{ label: loginState === "logged in" ? `${SESSION_MENU.login} \u2014 Already logged in` : SESSION_MENU.login, run: () => {
|
|
1220
|
+
void logIn();
|
|
1221
|
+
} },
|
|
1222
|
+
{ label: SESSION_MENU.status, run: () => {
|
|
1223
|
+
void showStatus();
|
|
1224
|
+
} },
|
|
1225
|
+
{ label: SESSION_MENU.logout, run: () => {
|
|
1226
|
+
setNotice("");
|
|
1227
|
+
setError("");
|
|
1228
|
+
setScreen("confirmLogout");
|
|
1229
|
+
} },
|
|
1230
|
+
...COMMAND_GROUPS.map((group) => ({ label: group.label, run: () => {
|
|
1231
|
+
setNotice("");
|
|
1232
|
+
setError("");
|
|
1233
|
+
setCommandGroup(group);
|
|
1234
|
+
setScreen("commands");
|
|
1235
|
+
} })),
|
|
1236
|
+
...webActions ? [{ label: "Web", run: () => {
|
|
1237
|
+
setNotice("");
|
|
1238
|
+
setError("");
|
|
1239
|
+
setScreen("web");
|
|
1240
|
+
} }] : [],
|
|
1241
|
+
{ label: "Quit", run: () => exit() }
|
|
1242
|
+
];
|
|
1243
|
+
useInput3((input, key) => {
|
|
1244
|
+
if (screen === "environment") {
|
|
1245
|
+
const index = environmentIndex ?? 0;
|
|
1246
|
+
const step = cursorStep(key);
|
|
1247
|
+
if (step) setEnvironmentIndex(wrapCursor(index, step, environments.length));
|
|
1248
|
+
else if (key.return) {
|
|
1249
|
+
const choice = environments[index].environment;
|
|
1250
|
+
void preferences.saveEnvironment(choice).catch(() => void 0);
|
|
1251
|
+
setReport(void 0);
|
|
1252
|
+
setError("");
|
|
1253
|
+
setMenuIndex(0);
|
|
1254
|
+
setEnvironment(choice);
|
|
1255
|
+
setScreen("menu");
|
|
1256
|
+
}
|
|
1257
|
+
return;
|
|
1258
|
+
}
|
|
1259
|
+
if (screen === "menu") {
|
|
1260
|
+
if (key.escape) {
|
|
1261
|
+
statusRequest.current += 1;
|
|
1262
|
+
setEnvironmentIndex(void 0);
|
|
1263
|
+
setScreen("environment");
|
|
1264
|
+
} else if (cursorStep(key)) setMenuIndex((index) => wrapCursor(index, cursorStep(key), menu.length));
|
|
1265
|
+
else if (key.return) menu[menuIndex]?.run();
|
|
1266
|
+
return;
|
|
1267
|
+
}
|
|
1268
|
+
if (screen === "credentialMissing") {
|
|
1269
|
+
if (key.return) setScreen("provision");
|
|
1270
|
+
else if (key.escape) backToMenu({});
|
|
1271
|
+
return;
|
|
1272
|
+
}
|
|
1273
|
+
if (screen === "status") {
|
|
1274
|
+
if (key.escape || key.return) backToMenu({});
|
|
1275
|
+
return;
|
|
1276
|
+
}
|
|
1277
|
+
if (screen === "confirmLogout") {
|
|
1278
|
+
if (input === "y" || input === "Y") void logOut();
|
|
1279
|
+
else if (input || key.return || key.escape) backToMenu({ notice: "Still logged in; nothing changed." });
|
|
1280
|
+
return;
|
|
1281
|
+
}
|
|
1282
|
+
if (screen === "otp") {
|
|
1283
|
+
if (key.escape) {
|
|
1284
|
+
attemptId.current = void 0;
|
|
1285
|
+
backToMenu({});
|
|
1286
|
+
} else if (key.return) void submitOtp();
|
|
1287
|
+
else if (key.backspace || key.delete) setOtp((value) => value.slice(0, -1));
|
|
1288
|
+
else if (input && !key.ctrl && !key.meta) setOtp((value) => (value + input.replace(/\D/g, "")).slice(0, 6));
|
|
1289
|
+
}
|
|
1290
|
+
});
|
|
1291
|
+
const header = screen === "environment" || !selected ? "Live Market Terminal" : [
|
|
1292
|
+
selected.environment,
|
|
1293
|
+
selected.origin,
|
|
1294
|
+
loginState ?? "checking\u2026",
|
|
1295
|
+
...props.profile ? [`profile: ${props.profile}`] : [],
|
|
1296
|
+
...props.device ? [`device: ${props.device}`] : []
|
|
1297
|
+
].join(" \xB7 ");
|
|
1298
|
+
return /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", gap: 1, children: [
|
|
1299
|
+
/* @__PURE__ */ jsx3(Text3, { bold: true, children: header }),
|
|
1300
|
+
screen === "environment" && environmentIndex !== void 0 && /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", children: [
|
|
1301
|
+
/* @__PURE__ */ jsx3(Text3, { children: "Choose the Deployment Environment" }),
|
|
1302
|
+
environments.map((option, index) => /* @__PURE__ */ jsxs3(Text3, { color: index === environmentIndex ? "cyan" : void 0, children: [
|
|
1303
|
+
index === environmentIndex ? "\u203A" : " ",
|
|
1304
|
+
" ",
|
|
1305
|
+
option.environment.padEnd(6),
|
|
1306
|
+
" ",
|
|
1307
|
+
option.origin
|
|
1308
|
+
] }, option.environment)),
|
|
1309
|
+
/* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "\u2191/\u2193 to move \xB7 Enter to choose" })
|
|
1310
|
+
] }),
|
|
1311
|
+
screen === "menu" && /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", children: [
|
|
1312
|
+
menu.map((item, index) => /* @__PURE__ */ jsxs3(Text3, { color: index === menuIndex ? "cyan" : void 0, children: [
|
|
1313
|
+
index === menuIndex ? "\u203A" : " ",
|
|
1314
|
+
" ",
|
|
1315
|
+
item.label
|
|
1316
|
+
] }, item.label)),
|
|
1317
|
+
/* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "\u2191/\u2193 to move \xB7 Enter to choose \xB7 Esc to change environment" })
|
|
1318
|
+
] }),
|
|
1319
|
+
screen === "busy" && /* @__PURE__ */ jsx3(Text3, { children: busy }),
|
|
1320
|
+
screen === "web" && selected && webActions && /* @__PURE__ */ jsx3(
|
|
1321
|
+
WebApp,
|
|
1322
|
+
{
|
|
1323
|
+
environment: selected.environment,
|
|
1324
|
+
origin: selected.origin,
|
|
1325
|
+
actions: webActions,
|
|
1326
|
+
...props.device ? { device: props.device } : {},
|
|
1327
|
+
onBack: () => {
|
|
1328
|
+
backToMenu({});
|
|
1329
|
+
void refreshStatus();
|
|
1330
|
+
},
|
|
1331
|
+
onLogIn: () => {
|
|
1332
|
+
void logIn();
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1335
|
+
),
|
|
1336
|
+
screen === "otp" && /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", children: [
|
|
1337
|
+
/* @__PURE__ */ jsx3(Text3, { children: "Zerodha OTP" }),
|
|
1338
|
+
/* @__PURE__ */ jsxs3(Text3, { children: [
|
|
1339
|
+
"\u2022".repeat(otpLength),
|
|
1340
|
+
/* @__PURE__ */ jsx3(Text3, { inverse: true, children: " " })
|
|
1341
|
+
] }),
|
|
1342
|
+
/* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "Enter to submit \xB7 Esc to cancel" })
|
|
1343
|
+
] }),
|
|
1344
|
+
screen === "credentialMissing" && /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", children: [
|
|
1345
|
+
/* @__PURE__ */ jsx3(Text3, { children: "SMS credential missing. Run `lmt credential provision` first." }),
|
|
1346
|
+
/* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "Enter to provision it now \xB7 Esc to return" })
|
|
1347
|
+
] }),
|
|
1348
|
+
screen === "commands" && commandGroup && environment && /* @__PURE__ */ jsx3(
|
|
1349
|
+
CommandMenu,
|
|
1350
|
+
{
|
|
1351
|
+
group: commandGroup,
|
|
1352
|
+
actions: actionsFor(environment),
|
|
1353
|
+
memory: commandMemory.current,
|
|
1354
|
+
onBack: () => backToMenu({}),
|
|
1355
|
+
onLogIn: () => {
|
|
1356
|
+
void logIn();
|
|
1357
|
+
},
|
|
1358
|
+
onProvision: () => setScreen("provision")
|
|
1359
|
+
}
|
|
1360
|
+
),
|
|
1361
|
+
screen === "provision" && /* @__PURE__ */ jsx3(CredentialProvisionApp, { store: props.store, onSaved: () => {
|
|
1362
|
+
backToMenu({ notice: "SMS credential saved." });
|
|
1363
|
+
void refreshStatus();
|
|
1364
|
+
} }),
|
|
1365
|
+
screen === "status" && report && /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", children: [
|
|
1366
|
+
/* @__PURE__ */ jsx3(Text3, { children: formatStatus(report) }),
|
|
1367
|
+
/* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "Enter or Esc to return" })
|
|
1368
|
+
] }),
|
|
1369
|
+
screen === "confirmLogout" && selected && /* @__PURE__ */ jsxs3(Text3, { children: [
|
|
1370
|
+
"Log out of ",
|
|
1371
|
+
selected.environment,
|
|
1372
|
+
props.profile ? ` profile ${props.profile}` : " everywhere",
|
|
1373
|
+
"? y/N"
|
|
1374
|
+
] }),
|
|
1375
|
+
notice && screen === "menu" ? /* @__PURE__ */ jsx3(Text3, { color: "green", children: notice }) : null,
|
|
1376
|
+
screen === "menu" && report?.error ? /* @__PURE__ */ jsx3(Text3, { color: "red", children: report.error.message }) : null,
|
|
1377
|
+
error ? /* @__PURE__ */ jsx3(Text3, { color: "red", children: error }) : null
|
|
1378
|
+
] });
|
|
1379
|
+
}
|
|
1380
|
+
export {
|
|
1381
|
+
ControllerApp
|
|
1382
|
+
};
|