@ai-digital-factory/lmt-web-cli 0.1.4 → 0.1.5

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.
@@ -0,0 +1,308 @@
1
+ import {
2
+ parseInteractiveArguments,
3
+ requestSession,
4
+ startSession
5
+ } from "./chunk-G5T7UDIK.js";
6
+
7
+ // src/interactive-main.ts
8
+ import { createElement } from "react";
9
+ import { render } from "ink";
10
+
11
+ // src/interactive-app.tsx
12
+ import { Box, Text, useApp, useInput } from "ink";
13
+ import { useRef, useState } from "react";
14
+
15
+ // src/interactive-login.ts
16
+ var RESTORE_DEADLINE_MS = 3e4;
17
+ var RESTORE_POLL_MS = 500;
18
+ var UNCERTAIN_MESSAGE = "The browser could not confirm the result. Check the page with `lmt-web inspect` before trying again; nothing was resubmitted.";
19
+ var DEFINITE_OTP_FAILURES = /* @__PURE__ */ new Set(["otp_rejected", "attempt_replaced", "attempt_unavailable", "attempt_failed", "invalid_otp", "invalid_session_id"]);
20
+ var defaultWait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
21
+ async function continueZerodhaLogin(session, options = {}) {
22
+ options.onStage?.("restoring");
23
+ return advanceLogin(session, await session.start(), options, { operatorLoginTried: false, earlierAttemptCancelled: false, attemptStarted: false, otpSubmitted: false });
24
+ }
25
+ async function logoutZerodha(session, options = {}) {
26
+ const wait = options.wait ?? defaultWait;
27
+ const now = options.now ?? Date.now;
28
+ options.onStage?.("restoring");
29
+ let current = await session.start();
30
+ let restoreDeadline;
31
+ for (; ; ) {
32
+ if (current.outcome === "uncertain") return { kind: "uncertain", message: UNCERTAIN_MESSAGE };
33
+ if (current.outcome === "failure") return { kind: "failed", message: current.error?.message ?? "The browser action failed." };
34
+ const sessionId = current.sessionId;
35
+ if (!sessionId) return { kind: "failed", message: "The browser session did not return a session ID." };
36
+ switch (current.authentication.status) {
37
+ case "zerodha_login_required":
38
+ case "operator_login_required":
39
+ return { kind: "logged_out", alreadyLoggedOut: true };
40
+ case "zerodha_session_restoring":
41
+ restoreDeadline ??= now() + RESTORE_DEADLINE_MS;
42
+ await wait(RESTORE_POLL_MS);
43
+ if (now() >= restoreDeadline) return { kind: "failed", message: "The Zerodha session is still restoring. Check the page with `lmt-web inspect`." };
44
+ current = await session.run(sessionId, "inspect");
45
+ continue;
46
+ case "authenticated": {
47
+ options.onStage?.("logging_out");
48
+ const result = await session.run(sessionId, "zerodha-logout");
49
+ if (result.outcome === "uncertain") return { kind: "uncertain", message: UNCERTAIN_MESSAGE };
50
+ if (result.outcome === "failure") return { kind: "failed", message: result.error?.message ?? "Zerodha logout failed." };
51
+ if (result.authentication.status === "zerodha_login_required") return { kind: "logged_out", alreadyLoggedOut: false };
52
+ return { kind: "failed", message: "Zerodha logout was not confirmed. Check the page with `lmt-web inspect`." };
53
+ }
54
+ default:
55
+ return { kind: "failed", message: "The browser page was not recognized as a Live Market Terminal surface." };
56
+ }
57
+ }
58
+ }
59
+ async function submitZerodhaOtp(session, input, options = {}) {
60
+ options.onStage?.("submitting_otp");
61
+ const submitted = await session.submitOtp(input.sessionId, input.attemptId, input.otp);
62
+ const authentication = submitted.authentication;
63
+ if (submitted.outcome === "failure") {
64
+ if (!DEFINITE_OTP_FAILURES.has(submitted.error?.code ?? "")) return { kind: "uncertain", message: UNCERTAIN_MESSAGE };
65
+ if (submitted.error?.code === "otp_rejected" && authentication.status === "zerodha_login_required" && authentication.nextAction === "zerodha-otp" && authentication.attemptId === input.attemptId) {
66
+ return { kind: "otp_required", sessionId: input.sessionId, attemptId: input.attemptId, message: submitted.error.message };
67
+ }
68
+ }
69
+ return advanceLogin(session, submitted, options, { operatorLoginTried: true, earlierAttemptCancelled: true, attemptStarted: true, otpSubmitted: true });
70
+ }
71
+ async function advanceLogin(session, initial, options, progress) {
72
+ const wait = options.wait ?? defaultWait;
73
+ const now = options.now ?? Date.now;
74
+ const tookAction = () => progress.otpSubmitted || progress.operatorLoginTried || progress.attemptStarted;
75
+ let current = initial;
76
+ let restoreDeadline;
77
+ for (; ; ) {
78
+ if (current.outcome === "uncertain") return { kind: "uncertain", message: UNCERTAIN_MESSAGE };
79
+ if (current.outcome === "failure") return { kind: "failed", message: current.error?.message ?? "The browser action failed." };
80
+ const sessionId = current.sessionId;
81
+ if (!sessionId) return { kind: "failed", message: "The browser session did not return a session ID." };
82
+ const authentication = current.authentication;
83
+ switch (authentication.status) {
84
+ case "authenticated":
85
+ return { kind: "authenticated", alreadyAuthenticated: !tookAction() };
86
+ case "operator_login_required":
87
+ if (progress.operatorLoginTried) return { kind: "failed", message: "Operator login did not complete. Check the Operator credentials in SMS." };
88
+ progress.operatorLoginTried = true;
89
+ options.onStage?.("operator_login");
90
+ current = await session.run(sessionId, "operator-login");
91
+ continue;
92
+ case "zerodha_login_required":
93
+ if (progress.attemptStarted && authentication.nextAction === "zerodha-otp") return { kind: "otp_required", sessionId, attemptId: authentication.attemptId };
94
+ if (!progress.attemptStarted && authentication.nextAction !== "zerodha-login") {
95
+ if (progress.earlierAttemptCancelled) return { kind: "failed", message: "An earlier Zerodha Login Attempt could not be cancelled. Check the page with `lmt-web inspect`." };
96
+ progress.earlierAttemptCancelled = true;
97
+ options.onStage?.("cancelling_attempt");
98
+ current = await session.run(sessionId, "zerodha-cancel");
99
+ continue;
100
+ }
101
+ if (progress.attemptStarted) return { kind: "failed", message: "The Zerodha Login Attempt is no longer accepting an OTP. Choose Login to Zerodha again." };
102
+ progress.attemptStarted = true;
103
+ options.onStage?.("starting_attempt");
104
+ current = await session.run(sessionId, "zerodha-login");
105
+ continue;
106
+ case "zerodha_session_restoring":
107
+ if (restoreDeadline === void 0) {
108
+ restoreDeadline = now() + RESTORE_DEADLINE_MS;
109
+ options.onStage?.("awaiting_session");
110
+ }
111
+ await wait(RESTORE_POLL_MS);
112
+ if (now() >= restoreDeadline) return { kind: "failed", message: "The Zerodha session is still restoring. Check the page with `lmt-web inspect`." };
113
+ current = await session.run(sessionId, "inspect");
114
+ continue;
115
+ default:
116
+ return { kind: "failed", message: "The browser page was not recognized as a Live Market Terminal login surface." };
117
+ }
118
+ }
119
+ }
120
+ function createInteractiveSession(deps) {
121
+ let startedSessionId;
122
+ let inFlight = 0;
123
+ const track = async (operation) => {
124
+ inFlight += 1;
125
+ try {
126
+ return await operation();
127
+ } finally {
128
+ inFlight -= 1;
129
+ }
130
+ };
131
+ return {
132
+ start: () => track(async () => {
133
+ const started = await deps.start();
134
+ if (started.result.sessionId) {
135
+ if (started.startedOwner) startedSessionId = started.result.sessionId;
136
+ else if (startedSessionId !== started.result.sessionId) startedSessionId = void 0;
137
+ }
138
+ return started.result;
139
+ }),
140
+ run: (sessionId, action) => track(() => deps.request({ sessionId, action })),
141
+ submitOtp: (sessionId, attemptId, otp) => track(() => deps.request({ sessionId, action: "zerodha-otp", attemptId, otp })),
142
+ async release() {
143
+ if (!startedSessionId || inFlight > 0) return;
144
+ const sessionId = startedSessionId;
145
+ startedSessionId = void 0;
146
+ await deps.request({ sessionId, action: "stop" });
147
+ }
148
+ };
149
+ }
150
+
151
+ // src/interactive-app.tsx
152
+ import { jsx, jsxs } from "react/jsx-runtime";
153
+ var STAGE_TEXT = {
154
+ restoring: "Restoring the saved browser session\u2026",
155
+ operator_login: "Completing Operator login\u2026",
156
+ cancelling_attempt: "Cancelling an earlier Zerodha login attempt\u2026",
157
+ starting_attempt: "Starting Login with Zerodha\u2026",
158
+ submitting_otp: "Submitting OTP\u2026",
159
+ awaiting_session: "Waiting for the Zerodha session\u2026",
160
+ logging_out: "Logging out of Zerodha\u2026"
161
+ };
162
+ var NOTICE_COLOR = { success: "green", error: "red", info: void 0 };
163
+ function InteractiveApp({ session, environment, device, onQuit }) {
164
+ const { exit } = useApp();
165
+ const [step, setStep] = useState({ kind: "menu" });
166
+ const [selected, setSelected] = useState(0);
167
+ const [notice, setNotice] = useState(null);
168
+ const [otp, setOtp] = useState("");
169
+ const [otpError, setOtpError] = useState("");
170
+ const busy = useRef(false);
171
+ const onStage = (stage) => setStep({ kind: "working", stage });
172
+ async function settle(action) {
173
+ busy.current = true;
174
+ setNotice(null);
175
+ let outcome;
176
+ try {
177
+ outcome = await action();
178
+ } catch (error) {
179
+ outcome = { kind: "failed", message: error instanceof Error ? error.message : "The browser action failed." };
180
+ }
181
+ busy.current = false;
182
+ if (outcome.kind === "otp_required") {
183
+ setOtp("");
184
+ setOtpError(outcome.message ?? "");
185
+ setStep({ kind: "otp", sessionId: outcome.sessionId, attemptId: outcome.attemptId });
186
+ return;
187
+ }
188
+ setStep({ kind: "menu" });
189
+ if (outcome.kind === "authenticated") {
190
+ setNotice({ tone: "success", text: outcome.alreadyAuthenticated ? "Already logged in to Zerodha." : "Logged in to Zerodha." });
191
+ } else if (outcome.kind === "logged_out") {
192
+ setNotice({ tone: "success", text: outcome.alreadyLoggedOut ? "Already logged out of Zerodha." : "Logged out of Zerodha." });
193
+ } else {
194
+ setNotice({ tone: "error", text: outcome.message });
195
+ }
196
+ }
197
+ const items = [
198
+ { label: "Login to Zerodha", run: () => {
199
+ void settle(() => continueZerodhaLogin(session, { onStage }));
200
+ } },
201
+ { label: "Logout from Zerodha", run: () => {
202
+ setNotice(null);
203
+ setStep({ kind: "confirm_logout" });
204
+ } }
205
+ ];
206
+ const active = Math.min(selected, items.length - 1);
207
+ function submitOtp(value) {
208
+ if (step.kind !== "otp") return;
209
+ if (!/^\d{6}$/.test(value)) {
210
+ setOtp(value);
211
+ setOtpError("Enter the six digits of the OTP.");
212
+ return;
213
+ }
214
+ setOtp("");
215
+ const { sessionId, attemptId } = step;
216
+ void settle(() => submitZerodhaOtp(session, { sessionId, attemptId, otp: value }, { onStage }));
217
+ }
218
+ useInput((input, key) => {
219
+ if (busy.current) return;
220
+ if (step.kind === "menu") {
221
+ if (key.upArrow) setSelected((index) => (index - 1 + items.length) % items.length);
222
+ else if (key.downArrow) setSelected((index) => (index + 1) % items.length);
223
+ else if (key.return) items[active]?.run();
224
+ else if (input === "q") {
225
+ onQuit?.();
226
+ exit();
227
+ }
228
+ return;
229
+ }
230
+ if (step.kind === "confirm_logout") {
231
+ if (input === "y" || input === "Y") void settle(() => logoutZerodha(session, { onStage }));
232
+ else {
233
+ setStep({ kind: "menu" });
234
+ setNotice({ tone: "info", text: "Logout cancelled." });
235
+ }
236
+ return;
237
+ }
238
+ if (step.kind === "otp") {
239
+ if (key.escape) {
240
+ setOtp("");
241
+ setStep({ kind: "menu" });
242
+ setNotice({ tone: "info", text: "Zerodha login cancelled. No OTP was submitted." });
243
+ } else if (key.return) submitOtp(otp);
244
+ else if (key.backspace || key.delete) setOtp((value) => value.slice(0, -1));
245
+ else if (input && !key.ctrl && !key.meta) {
246
+ const value = (otp + input.replace(/\D/g, "")).slice(0, 6);
247
+ if (/[\r\n]/.test(input)) submitOtp(value);
248
+ else setOtp(value);
249
+ }
250
+ }
251
+ });
252
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", gap: 1, children: [
253
+ /* @__PURE__ */ jsx(Text, { bold: true, children: "Live Market Terminal Web" }),
254
+ step.kind === "menu" && /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
255
+ items.map((item, index) => /* @__PURE__ */ jsxs(Text, { color: index === active ? "cyan" : void 0, bold: index === active, children: [
256
+ index === active ? "\u203A" : " ",
257
+ " ",
258
+ item.label
259
+ ] }, item.label)),
260
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2191/\u2193 to move \xB7 Enter to select \xB7 q to quit" })
261
+ ] }),
262
+ step.kind === "working" && /* @__PURE__ */ jsx(Text, { children: STAGE_TEXT[step.stage] }),
263
+ step.kind === "confirm_logout" && /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
264
+ /* @__PURE__ */ jsxs(Text, { children: [
265
+ "Log out of Zerodha for ",
266
+ environment,
267
+ " \xB7 ",
268
+ device,
269
+ "?"
270
+ ] }),
271
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "Agents restoring this saved session will need a new Zerodha login. y to confirm \xB7 any other key to cancel" })
272
+ ] }),
273
+ step.kind === "otp" && /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
274
+ /* @__PURE__ */ jsx(Text, { children: "Zerodha OTP" }),
275
+ /* @__PURE__ */ jsxs(Text, { children: [
276
+ "\u2022".repeat(otp.length),
277
+ /* @__PURE__ */ jsx(Text, { inverse: true, children: " " })
278
+ ] }),
279
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "Enter to submit \xB7 Esc to cancel" }),
280
+ otpError ? /* @__PURE__ */ jsx(Text, { color: "red", children: otpError }) : null
281
+ ] }),
282
+ notice ? /* @__PURE__ */ jsx(Text, { color: NOTICE_COLOR[notice.tone], children: notice.text }) : null,
283
+ /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
284
+ environment,
285
+ " \xB7 ",
286
+ device
287
+ ] })
288
+ ] });
289
+ }
290
+
291
+ // src/interactive-main.ts
292
+ var ACTION_TIMEOUT_MS = 3e4;
293
+ async function runInteractive(argv) {
294
+ const { environment, device } = parseInteractiveArguments(argv);
295
+ const session = createInteractiveSession({
296
+ start: () => startSession({ environment, mode: device, headless: true, fresh: false, timeoutMs: ACTION_TIMEOUT_MS }),
297
+ request: (request) => requestSession({ ...request, timeoutMs: ACTION_TIMEOUT_MS })
298
+ });
299
+ const app = render(createElement(InteractiveApp, { session, environment, device }));
300
+ try {
301
+ await app.waitUntilExit();
302
+ } finally {
303
+ await session.release().catch(() => void 0);
304
+ }
305
+ }
306
+ export {
307
+ runInteractive
308
+ };
package/package.json CHANGED
@@ -1,12 +1,14 @@
1
1
  {
2
2
  "name": "@ai-digital-factory/lmt-web-cli",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "lmt-web": "dist/index.js"
7
7
  },
8
8
  "dependencies": {
9
- "playwright": "^1.55.0"
9
+ "ink": "7.1.1",
10
+ "playwright": "^1.55.0",
11
+ "react": "19.2.8"
10
12
  },
11
13
  "description": "Standalone browser session CLI for Live Market Terminal Web.",
12
14
  "engines": {