@ai-digital-factory/lmt-web-cli 0.1.2 → 0.1.3
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 +38 -1
- package/dist/index.js +169 -16
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -115,6 +115,11 @@ Quitting the browser from its Dock menu or closing its session window also ends
|
|
|
115
115
|
the browser session and its local owner. This does not log out the Operator or
|
|
116
116
|
stop Broker activity. A later `inspect` reports `session_not_found`.
|
|
117
117
|
|
|
118
|
+
On Dev Deployment, Dev Operator opens the Web UI with `?captureSafe=1` on the first
|
|
119
|
+
navigation (for example `https://dev-live-market-terminal.piyush-arora.in/?captureSafe=1`)
|
|
120
|
+
so Capture-safe Mode stays enabled for that tab through navigation and refresh.
|
|
121
|
+
`lmt-web` screenshots still mask access-token value fields automatically without that flag.
|
|
122
|
+
|
|
118
123
|
```sh
|
|
119
124
|
lmt-web start --env dev
|
|
120
125
|
lmt-web inspect --session SESSION_ID
|
|
@@ -141,7 +146,10 @@ Each command writes one versioned JSON result to stdout and diagnostics to
|
|
|
141
146
|
stderr. Results contain the action outcome, current URL, visible page text and
|
|
142
147
|
controls, emulated device and actual viewport, and screenshot outcome. Images
|
|
143
148
|
are written to unique absolute paths in an owner-only temporary directory;
|
|
144
|
-
screenshots
|
|
149
|
+
screenshots solid-mask only Zerodha access-token value fields marked with
|
|
150
|
+
`data-lmt-sensitive="zerodha-access-token"`; other controls and metadata stay visible.
|
|
151
|
+
If protected capture fails, the screenshot outcome is `failed` with no unmasked fallback.
|
|
152
|
+
Field values
|
|
145
153
|
are omitted from JSON observations. Captures wait for document loading, visible
|
|
146
154
|
session-loading or busy indicators, fonts, visible images, and finite animations
|
|
147
155
|
to finish. The page must render text, controls, or visual content; an empty app
|
|
@@ -321,3 +329,32 @@ sets up its own Chromium runtime, verifies packaged runtime modules/configuratio
|
|
|
321
329
|
and reuses all browser acceptance tests. Installed commands and their live owners
|
|
322
330
|
run outside the checkout. Test-only fixture servers provide repeatable responses;
|
|
323
331
|
this check performs no npm publication or live dev login.
|
|
332
|
+
|
|
333
|
+
## Explicit values, equality, scrolling, and full content
|
|
334
|
+
|
|
335
|
+
```sh
|
|
336
|
+
lmt-web read --session ID --ref FIELD
|
|
337
|
+
lmt-web clipboard --session ID
|
|
338
|
+
lmt-web check --session ID --ref FIELD --stdin < /path/to/protected-expectation
|
|
339
|
+
lmt-web clipboard-check --session ID --ref FIELD
|
|
340
|
+
lmt-web scroll --session ID --ref FIELD
|
|
341
|
+
lmt-web scroll --session ID --y 600
|
|
342
|
+
lmt-web inspect --session ID --full-page
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
read returns an ordinary field value. clipboard returns content only when it matches
|
|
346
|
+
a verified ordinary field on the current page; unknown or secret content is refused.
|
|
347
|
+
check compares the field with one stdin-supplied expectation (an optional final newline
|
|
348
|
+
is removed). clipboard-check compares clipboard and field inside the browser session.
|
|
349
|
+
Comparisons return only matches: true or matches: false. An unreadable field/clipboard
|
|
350
|
+
or an expired reference is an error without a matches result. Inspect/action observations
|
|
351
|
+
continue to redact secrets, including revealed fields.
|
|
352
|
+
|
|
353
|
+
Referenced scrolling brings the control into view through its ancestors. Pixel scrolling
|
|
354
|
+
targets the largest scrolling content area. Results include before/after positions and
|
|
355
|
+
moved, already_visible, or at_boundary. Each observation expires older references.
|
|
356
|
+
|
|
357
|
+
Full-page capture temporarily expands inner scrolling content and restores its styles
|
|
358
|
+
and positions afterward. A screenshot with unexpanded clipped content reports failed;
|
|
359
|
+
browser-action and screenshot outcomes remain separate. Screenshots retain the existing
|
|
360
|
+
rendered-page confidentiality behavior: protect their files as you would the visible UI.
|
package/dist/index.js
CHANGED
|
@@ -511,6 +511,13 @@ var ACTIONS = {
|
|
|
511
511
|
return { action: "zerodha-otp", attemptId, otp: await input.readOtp() };
|
|
512
512
|
}
|
|
513
513
|
},
|
|
514
|
+
read: { control: true, flags: [], values: ["--ref"], build: (input) => ({ action: "read", reference: required(input, "--ref") }) },
|
|
515
|
+
clipboard: { control: true, flags: [], values: [], build: () => ({ action: "clipboard" }) },
|
|
516
|
+
check: { control: true, flags: ["--stdin"], values: ["--ref"], build: async (input) => {
|
|
517
|
+
if (!input.flags.has("--stdin")) throw new InvocationError("check requires --stdin.");
|
|
518
|
+
return { action: "check", reference: required(input, "--ref"), expected: await input.readInput() };
|
|
519
|
+
} },
|
|
520
|
+
"clipboard-check": { control: true, flags: [], values: ["--ref"], build: (input) => ({ action: "clipboard-check", reference: required(input, "--ref") }) },
|
|
514
521
|
click: { control: true, flags: [], values: ["--ref"], build: (input) => ({ action: "click", reference: required(input, "--ref") }) },
|
|
515
522
|
fill: {
|
|
516
523
|
control: true,
|
|
@@ -614,6 +621,52 @@ import os from "node:os";
|
|
|
614
621
|
import path4 from "node:path";
|
|
615
622
|
import { chromium as chromium2 } from "playwright";
|
|
616
623
|
|
|
624
|
+
// ../../packages/crawler/src/full-content.ts
|
|
625
|
+
async function captureFullContent(browserPage, options) {
|
|
626
|
+
const page = browserPage;
|
|
627
|
+
if (!page.evaluate) throw new Error("Full-content capture is unavailable.");
|
|
628
|
+
try {
|
|
629
|
+
await page.evaluate(() => {
|
|
630
|
+
const state = [];
|
|
631
|
+
window.__lmtCapture = state;
|
|
632
|
+
const scrolling = Array.from(document.querySelectorAll("body *")).filter((element) => {
|
|
633
|
+
const style = getComputedStyle(element);
|
|
634
|
+
return /(auto|scroll)/.test(style.overflowY) && element.scrollHeight > element.clientHeight;
|
|
635
|
+
});
|
|
636
|
+
const expand = /* @__PURE__ */ new Set();
|
|
637
|
+
for (const element of scrolling) {
|
|
638
|
+
for (let parent = element; parent; parent = parent.parentElement) expand.add(parent);
|
|
639
|
+
}
|
|
640
|
+
for (const element of expand) {
|
|
641
|
+
state.push({ element, style: element.getAttribute("style"), x: element.scrollLeft, y: element.scrollTop });
|
|
642
|
+
element.style.setProperty("height", "auto", "important");
|
|
643
|
+
element.style.setProperty("max-height", "none", "important");
|
|
644
|
+
element.style.setProperty("overflow", "visible", "important");
|
|
645
|
+
element.style.setProperty("flex-shrink", "0", "important");
|
|
646
|
+
element.scrollTop = 0;
|
|
647
|
+
}
|
|
648
|
+
});
|
|
649
|
+
await page.evaluate(() => new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(() => resolve()))));
|
|
650
|
+
const complete = await page.evaluate(() => Array.from(document.querySelectorAll("body *")).every((element) => {
|
|
651
|
+
const style = getComputedStyle(element);
|
|
652
|
+
return style.display === "none" || element.clientHeight === 0 || !/(auto|scroll|hidden|clip)/.test(style.overflowY) || element.scrollHeight <= element.clientHeight + 1;
|
|
653
|
+
}));
|
|
654
|
+
if (!complete) throw new Error("Full-content capture could not expand all clipped content.");
|
|
655
|
+
return await page.screenshot({ ...options, fullPage: true });
|
|
656
|
+
} finally {
|
|
657
|
+
await page.evaluate(() => {
|
|
658
|
+
const owner = window;
|
|
659
|
+
for (const { element, style, x, y } of (owner.__lmtCapture ?? []).reverse()) {
|
|
660
|
+
if (style === null) element.removeAttribute("style");
|
|
661
|
+
else element.setAttribute("style", style);
|
|
662
|
+
element.scrollLeft = x;
|
|
663
|
+
element.scrollTop = y;
|
|
664
|
+
}
|
|
665
|
+
delete owner.__lmtCapture;
|
|
666
|
+
}).catch(() => void 0);
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
|
|
617
670
|
// ../../packages/crawler/src/controls.ts
|
|
618
671
|
import { randomBytes } from "node:crypto";
|
|
619
672
|
var ControlError = class extends Error {
|
|
@@ -623,6 +676,7 @@ var ControlError = class extends Error {
|
|
|
623
676
|
}
|
|
624
677
|
code;
|
|
625
678
|
};
|
|
679
|
+
var SENSITIVE_FIELD_PATTERN = "password|credential|secret|token|otp|one.time|username|user.?id|api.?key|pin";
|
|
626
680
|
function controlSignature(node) {
|
|
627
681
|
if (!node.isConnected) return null;
|
|
628
682
|
return JSON.stringify([
|
|
@@ -657,8 +711,8 @@ var PageControls = class {
|
|
|
657
711
|
return result;
|
|
658
712
|
}
|
|
659
713
|
async learnSecrets() {
|
|
660
|
-
const values = await this.page.evaluate(() => {
|
|
661
|
-
const sensitive =
|
|
714
|
+
const values = await this.page.evaluate((pattern) => {
|
|
715
|
+
const sensitive = new RegExp(pattern, "i");
|
|
662
716
|
const found = Array.from(document.querySelectorAll("input, textarea")).filter((element) => sensitive.test([
|
|
663
717
|
element.outerHTML.replace(/value=("[^"]*"|'[^']*')/g, ""),
|
|
664
718
|
...Array.from(element.labels ?? []).map((label) => label.innerText)
|
|
@@ -667,15 +721,16 @@ var PageControls = class {
|
|
|
667
721
|
if (match[1]) found.push(match[1]);
|
|
668
722
|
}
|
|
669
723
|
return found;
|
|
670
|
-
});
|
|
724
|
+
}, SENSITIVE_FIELD_PATTERN);
|
|
671
725
|
this.registerSecrets(...values);
|
|
672
726
|
}
|
|
673
727
|
async screenshotMasks() {
|
|
728
|
+
const stable = this.page.locator('[data-lmt-sensitive="zerodha-access-token"]');
|
|
674
729
|
try {
|
|
675
730
|
await this.learnSecrets();
|
|
676
731
|
} catch (error) {
|
|
677
732
|
if (this.secrets.size) throw error;
|
|
678
|
-
else return [];
|
|
733
|
+
else return [stable];
|
|
679
734
|
}
|
|
680
735
|
await this.page.evaluate((secrets) => {
|
|
681
736
|
document.querySelectorAll("[data-lmt-secret-mask]").forEach((element) => element.removeAttribute("data-lmt-secret-mask"));
|
|
@@ -685,7 +740,7 @@ var PageControls = class {
|
|
|
685
740
|
if (secrets.some((secret) => directText.includes(secret) || (element.textContent ?? "").includes(secret) && !Array.from(element.children).some((child) => (child.textContent ?? "").includes(secret)))) element.setAttribute("data-lmt-secret-mask", "");
|
|
686
741
|
}
|
|
687
742
|
}, [...this.secrets]);
|
|
688
|
-
return [this.page.locator("[data-lmt-secret-mask]")];
|
|
743
|
+
return [stable, this.page.locator("[data-lmt-secret-mask]")];
|
|
689
744
|
}
|
|
690
745
|
async inspect() {
|
|
691
746
|
await this.learnSecrets();
|
|
@@ -735,9 +790,52 @@ var PageControls = class {
|
|
|
735
790
|
async act(command, timeoutMs, onSubmit = () => void 0) {
|
|
736
791
|
this.page.setDefaultTimeout(timeoutMs);
|
|
737
792
|
this.page.setDefaultNavigationTimeout(timeoutMs);
|
|
793
|
+
if (command.action === "read" || command.action === "check" || command.action === "clipboard-check" || command.action === "clipboard") {
|
|
794
|
+
await this.learnSecrets();
|
|
795
|
+
if (command.action === "check") this.registerSecrets(command.expected);
|
|
796
|
+
try {
|
|
797
|
+
const field = command.action === "clipboard" ? void 0 : await this.resolve(command.reference);
|
|
798
|
+
if (command.action === "clipboard-check" || command.action === "clipboard") await this.page.context().grantPermissions(["clipboard-read", "clipboard-write"], { origin: this.origin });
|
|
799
|
+
if (command.action === "clipboard-check") {
|
|
800
|
+
const matches = await field.evaluate(async (node) => {
|
|
801
|
+
if (!(node instanceof HTMLInputElement || node instanceof HTMLTextAreaElement || node instanceof HTMLSelectElement)) throw new Error("Unreadable field");
|
|
802
|
+
return await navigator.clipboard.readText() === node.value;
|
|
803
|
+
});
|
|
804
|
+
return { matches };
|
|
805
|
+
}
|
|
806
|
+
const value = command.action === "clipboard" ? await this.page.evaluate(() => navigator.clipboard.readText()) : await field.evaluate((node) => {
|
|
807
|
+
if (!(node instanceof HTMLInputElement || node instanceof HTMLTextAreaElement || node instanceof HTMLSelectElement)) throw new Error("Unreadable field");
|
|
808
|
+
return node.value;
|
|
809
|
+
});
|
|
810
|
+
if (command.action === "check") {
|
|
811
|
+
this.registerSecrets(value);
|
|
812
|
+
return { matches: value === command.expected };
|
|
813
|
+
}
|
|
814
|
+
const sensitive = field && await field.evaluate((node, pattern) => new RegExp(pattern, "i").test(node.outerHTML.replace(/value=("[^"]*"|'[^']*')/g, "") + Array.from(node.labels ?? []).map((label) => label.innerText).join(" ")), SENSITIVE_FIELD_PATTERN);
|
|
815
|
+
if (command.action === "clipboard") {
|
|
816
|
+
const ordinary = await this.page.evaluate(({ value: value2, pattern }) => {
|
|
817
|
+
const sensitive2 = new RegExp(pattern, "i");
|
|
818
|
+
return Array.from(document.querySelectorAll("input,textarea,select")).some((node) => {
|
|
819
|
+
if (!(node instanceof HTMLInputElement || node instanceof HTMLTextAreaElement || node instanceof HTMLSelectElement)) return false;
|
|
820
|
+
const description = node.outerHTML.replace(/value=("[^"]*"|'[^']*')/g, "") + Array.from(node.labels ?? []).map((label) => label.innerText).join(" ");
|
|
821
|
+
return !sensitive2.test(description) && node.value === value2;
|
|
822
|
+
});
|
|
823
|
+
}, { value, pattern: SENSITIVE_FIELD_PATTERN });
|
|
824
|
+
if (!ordinary) {
|
|
825
|
+
this.registerSecrets(value);
|
|
826
|
+
throw new ControlError("secret_read_refused", "Clipboard content has no verified ordinary field source. Use clipboard-check for secret Copy.");
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
if (sensitive || [...this.secrets].some((secret) => value.includes(secret))) throw new ControlError("secret_read_refused", "Use an equality check for secret values.");
|
|
830
|
+
return { value };
|
|
831
|
+
} catch (error) {
|
|
832
|
+
if (error instanceof ControlError) throw error;
|
|
833
|
+
throw new ControlError("value_unreadable", "The field or clipboard could not be read. No comparison result is available.");
|
|
834
|
+
}
|
|
835
|
+
}
|
|
738
836
|
if (command.action === "fill") {
|
|
739
837
|
const element = await this.resolve(command.reference);
|
|
740
|
-
const sensitive = await element.evaluate((node) =>
|
|
838
|
+
const sensitive = await element.evaluate((node, pattern) => new RegExp(pattern, "i").test(node.outerHTML.replace(/value=("[^"]*"|'[^']*')/g, "") + Array.from(node.labels ?? []).map((label) => label.innerText).join(" ")), SENSITIVE_FIELD_PATTERN);
|
|
741
839
|
if (command.secret || sensitive) this.registerSecrets(command.value);
|
|
742
840
|
await element.fill(command.value, { timeout: timeoutMs });
|
|
743
841
|
return;
|
|
@@ -747,6 +845,7 @@ var PageControls = class {
|
|
|
747
845
|
return;
|
|
748
846
|
}
|
|
749
847
|
if (command.action === "click") {
|
|
848
|
+
await this.page.context().grantPermissions(["clipboard-read", "clipboard-write"], { origin: this.origin });
|
|
750
849
|
const element = await this.resolve(command.reference);
|
|
751
850
|
const deadlineAt = Date.now() + timeoutMs;
|
|
752
851
|
await element.click({ trial: true, timeout: timeoutMs });
|
|
@@ -768,12 +867,33 @@ var PageControls = class {
|
|
|
768
867
|
}
|
|
769
868
|
if (command.action === "scroll") {
|
|
770
869
|
if (!Number.isFinite(command.x) || !Number.isFinite(command.y)) throw new ControlError("invalid_invocation", "Scroll offsets must be finite pixel values.");
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
870
|
+
const element = command.reference ? await this.resolve(command.reference) : void 0;
|
|
871
|
+
const scroll = await this.page.evaluate(({ element: node, offsets }) => {
|
|
872
|
+
const scrollable = (item) => {
|
|
873
|
+
const style = getComputedStyle(item);
|
|
874
|
+
return /(auto|scroll)/.test(style.overflow + style.overflowY + style.overflowX) && (item.scrollHeight > item.clientHeight || item.scrollWidth > item.clientWidth);
|
|
875
|
+
};
|
|
876
|
+
const containers = [];
|
|
877
|
+
if (node) {
|
|
878
|
+
for (let parent = node.parentElement; parent; parent = parent.parentElement) if (scrollable(parent)) containers.push(parent);
|
|
879
|
+
if (document.scrollingElement && !containers.includes(document.scrollingElement)) containers.push(document.scrollingElement);
|
|
880
|
+
} else {
|
|
881
|
+
const candidates = Array.from(document.querySelectorAll("body *")).filter(scrollable);
|
|
882
|
+
const root = document.scrollingElement;
|
|
883
|
+
if (root && !/(hidden|clip)/.test(getComputedStyle(root).overflowY) && (root.scrollHeight > root.clientHeight || root.scrollWidth > root.clientWidth)) candidates.push(root);
|
|
884
|
+
candidates.sort((a, b) => b.clientWidth * b.clientHeight - a.clientWidth * a.clientHeight);
|
|
885
|
+
containers.push(candidates[0] ?? document.scrollingElement);
|
|
886
|
+
}
|
|
887
|
+
const positions = () => containers.map((item) => ({ x: item.scrollLeft, y: item.scrollTop }));
|
|
888
|
+
const before = positions();
|
|
889
|
+
if (node) node.scrollIntoView({ block: "nearest", inline: "nearest", behavior: "instant" });
|
|
890
|
+
else containers[0].scrollBy({ left: offsets.x, top: offsets.y, behavior: "instant" });
|
|
891
|
+
const after = positions();
|
|
892
|
+
const moved = before.some((position, i) => position.x !== after[i].x || position.y !== after[i].y);
|
|
893
|
+
return { outcome: moved ? "moved" : node ? "already_visible" : "at_boundary", before, after };
|
|
894
|
+
}, { element: element ?? null, offsets: command });
|
|
776
895
|
await this.page.evaluate(() => new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(() => resolve()))));
|
|
896
|
+
return { scroll };
|
|
777
897
|
}
|
|
778
898
|
}
|
|
779
899
|
};
|
|
@@ -1915,6 +2035,13 @@ async function inspectPage(page, mode, crawler) {
|
|
|
1915
2035
|
}
|
|
1916
2036
|
};
|
|
1917
2037
|
}
|
|
2038
|
+
var ACCESS_TOKEN_SCREENSHOT_MASK = '[data-lmt-sensitive="zerodha-access-token"]';
|
|
2039
|
+
async function screenshotMaskOptions(page) {
|
|
2040
|
+
const playwrightPage = page;
|
|
2041
|
+
const locator = playwrightPage.locator(ACCESS_TOKEN_SCREENSHOT_MASK);
|
|
2042
|
+
if (!await locator.count()) return {};
|
|
2043
|
+
return { mask: [locator], maskColor: "#000000" };
|
|
2044
|
+
}
|
|
1918
2045
|
async function captureScreenshot(page, sessionId, action, fullPage, timeoutMs, onTimeout) {
|
|
1919
2046
|
if (!page) return { outcome: "unavailable", reason: "No browser page was available." };
|
|
1920
2047
|
if (!page.screenshot) return { outcome: "failed", error: "The browser does not support screenshots." };
|
|
@@ -1930,12 +2057,15 @@ async function captureScreenshot(page, sessionId, action, fullPage, timeoutMs, o
|
|
|
1930
2057
|
await withDeadline(
|
|
1931
2058
|
(async () => {
|
|
1932
2059
|
await waitForRenderedPage(page);
|
|
1933
|
-
|
|
2060
|
+
const maskOptions = await screenshotMaskOptions(page);
|
|
2061
|
+
const options = {
|
|
1934
2062
|
path: screenshotPath,
|
|
1935
2063
|
fullPage,
|
|
1936
2064
|
animations: "disabled",
|
|
1937
|
-
timeout: timeoutMs
|
|
1938
|
-
|
|
2065
|
+
timeout: timeoutMs,
|
|
2066
|
+
...maskOptions
|
|
2067
|
+
};
|
|
2068
|
+
return fullPage ? captureFullContent(page, options) : page.screenshot(options);
|
|
1939
2069
|
})(),
|
|
1940
2070
|
timeoutMs,
|
|
1941
2071
|
onTimeout
|
|
@@ -2189,11 +2319,12 @@ async function runSessionOwner(options) {
|
|
|
2189
2319
|
guidedError = error2;
|
|
2190
2320
|
}
|
|
2191
2321
|
let inspected = null;
|
|
2322
|
+
let controlResult;
|
|
2192
2323
|
let actionError;
|
|
2193
2324
|
try {
|
|
2194
2325
|
if (isControlAction(request)) {
|
|
2195
2326
|
const budget = Math.max(1, remainingMs(deadlineAt) - Math.min(2e3, request.timeoutMs / 4));
|
|
2196
|
-
await withDeadline(crawler.controls().act(request, budget, () => {
|
|
2327
|
+
controlResult = await withDeadline(crawler.controls().act(request, budget, () => {
|
|
2197
2328
|
submitted = true;
|
|
2198
2329
|
}), budget, () => void 0);
|
|
2199
2330
|
}
|
|
@@ -2235,6 +2366,7 @@ async function runSessionOwner(options) {
|
|
|
2235
2366
|
sessionId: options.sessionId,
|
|
2236
2367
|
action: request.action,
|
|
2237
2368
|
outcome: error instanceof ControlError && error.code === "action_uncertain" ? "uncertain" : error ? submitted && !(error instanceof OperatorActionError) && !(error instanceof LoginAttemptError) ? "uncertain" : "failure" : "success",
|
|
2369
|
+
...!actionError ? controlResult : {},
|
|
2238
2370
|
...loginAttempt ? { loginAttempt: { attemptId: loginAttempt.attemptId, status: loginAttempt.status } } : {},
|
|
2239
2371
|
url: inspected?.url ?? (page ? safeUrl(crawler?.controls().redact(page.url()) ?? page.url()) : null),
|
|
2240
2372
|
visibleState: inspected?.visibleState ?? null,
|
|
@@ -2387,6 +2519,10 @@ Usage:
|
|
|
2387
2519
|
lmt-web inspect --session ID [--full-page] [--timeout 30s]
|
|
2388
2520
|
lmt-web click --session ID --ref REF
|
|
2389
2521
|
lmt-web fill --session ID --ref REF --value TEXT [--secret]
|
|
2522
|
+
lmt-web read --session ID --ref REF
|
|
2523
|
+
lmt-web clipboard --session ID
|
|
2524
|
+
lmt-web check --session ID --ref REF --stdin
|
|
2525
|
+
lmt-web clipboard-check --session ID --ref REF
|
|
2390
2526
|
lmt-web select --session ID --ref REF --value OPTION
|
|
2391
2527
|
lmt-web scroll --session ID [--ref REF] [--x PIXELS] [--y PIXELS]
|
|
2392
2528
|
lmt-web navigate --session ID --url PATH_OR_URL
|
|
@@ -2474,7 +2610,7 @@ async function parseCommand(argv) {
|
|
|
2474
2610
|
}
|
|
2475
2611
|
let payload;
|
|
2476
2612
|
try {
|
|
2477
|
-
payload = await definition.build({ values, flags, readOtp: () => readOtp(timeoutMs) });
|
|
2613
|
+
payload = await definition.build({ values, flags, readOtp: () => readOtp(timeoutMs), readInput: () => readInput(timeoutMs) });
|
|
2478
2614
|
} catch (error) {
|
|
2479
2615
|
throw new InvocationError(error instanceof Error ? error.message : "The action input could not be read.", error instanceof InvocationError ? error.code : "action_failed", action);
|
|
2480
2616
|
}
|
|
@@ -2487,6 +2623,23 @@ function emit(result, diagnostic) {
|
|
|
2487
2623
|
if (diagnostic) process.stderr.write(`${diagnostic}
|
|
2488
2624
|
`);
|
|
2489
2625
|
}
|
|
2626
|
+
async function readInput(timeoutMs) {
|
|
2627
|
+
if (process.stdin.isTTY) throw new InvocationError("Pipe the expected value on stdin.");
|
|
2628
|
+
let input = "";
|
|
2629
|
+
const timer = setTimeout(() => process.stdin.destroy(new Error("Input deadline exceeded.")), timeoutMs);
|
|
2630
|
+
try {
|
|
2631
|
+
for await (const chunk of process.stdin) {
|
|
2632
|
+
input += chunk.toString();
|
|
2633
|
+
if (Buffer.byteLength(input) > 16384) throw new InvocationError("Expected value is too large.");
|
|
2634
|
+
}
|
|
2635
|
+
return input.replace(/\r?\n$/, "");
|
|
2636
|
+
} catch {
|
|
2637
|
+
throw new InvocationError("Expected value could not be read from stdin.");
|
|
2638
|
+
} finally {
|
|
2639
|
+
input = "";
|
|
2640
|
+
clearTimeout(timer);
|
|
2641
|
+
}
|
|
2642
|
+
}
|
|
2490
2643
|
async function readOtp(timeoutMs) {
|
|
2491
2644
|
let input = "";
|
|
2492
2645
|
const timer = setTimeout(() => process.stdin.destroy(new Error("OTP input exceeded its deadline.")), timeoutMs);
|