replicas-cli 0.2.462 → 0.2.464
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/dist/index.mjs +163 -3061
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -7335,8 +7335,9 @@ var require_dist = __commonJS({
|
|
|
7335
7335
|
|
|
7336
7336
|
// src/index.ts
|
|
7337
7337
|
import "dotenv/config";
|
|
7338
|
+
import { spawnSync } from "child_process";
|
|
7338
7339
|
import { Command, InvalidArgumentError } from "commander";
|
|
7339
|
-
import
|
|
7340
|
+
import chalk23 from "chalk";
|
|
7340
7341
|
|
|
7341
7342
|
// src/commands/login.ts
|
|
7342
7343
|
import http from "http";
|
|
@@ -8397,6 +8398,7 @@ Then embed the printed \`\` line in your chat reply. See \`MEDI
|
|
|
8397
8398
|
|
|
8398
8399
|
## Failure modes
|
|
8399
8400
|
|
|
8401
|
+
- **"Computer-use daemon is unavailable"**: the package-managed systemd service did not start. Check \`systemctl status replicas-computer.service\` and its journal.
|
|
8400
8402
|
- **"Desktop services script missing"**: workspace image is older than this skill. Tell the user - nothing you can do from the CLI side.
|
|
8401
8403
|
- **\`xdotool ... failed: Can't open display\`**: Xvfb didn't come up. \`replicas computer status\` will show which service is dead and auto-repair the desktop bridge.
|
|
8402
8404
|
- **Browser doesn't appear after \`launch chrome\`**: run \`replicas computer observe /tmp/state.png\`. Chrome cold-start on the virtual display takes ~500ms but bigger pages take longer.
|
|
@@ -8408,7 +8410,8 @@ Then embed the printed \`\` line in your chat reply. See \`MEDI
|
|
|
8408
8410
|
| Component | Where |
|
|
8409
8411
|
|---|---|
|
|
8410
8412
|
| \`xvfb\`, \`openbox\`, \`tint2\`, \`x11vnc\`, \`websockify\`, \`xdotool\`, \`scrot\`, \`ffmpeg\`, \`google-chrome\` | Baked into the workspace image |
|
|
8411
|
-
|
|
|
8413
|
+
| \`replicas-computer\`, desktop assets, and service unit | Installed by \`replicas-computer setup\` during the image build |
|
|
8414
|
+
| Xvfb / openbox / tint2 / x11vnc / websockify processes | Managed by \`replicas-computer.service\` |
|
|
8412
8415
|
| noVNC preview URL (port 6080) | Registered by the engine on startup (authenticated) |
|
|
8413
8416
|
| Dashboard \`Desktop\` tab | Always available once the engine has registered the preview |
|
|
8414
8417
|
|
|
@@ -10078,7 +10081,7 @@ function formatTurnElapsed(ms) {
|
|
|
10078
10081
|
}
|
|
10079
10082
|
|
|
10080
10083
|
// ../shared/src/cli-version.ts
|
|
10081
|
-
var CLI_VERSION = "0.2.
|
|
10084
|
+
var CLI_VERSION = "0.2.464";
|
|
10082
10085
|
|
|
10083
10086
|
// ../shared/src/version.ts
|
|
10084
10087
|
function compareVersions(v1, v2) {
|
|
@@ -10093,16 +10096,6 @@ function compareVersions(v1, v2) {
|
|
|
10093
10096
|
return 0;
|
|
10094
10097
|
}
|
|
10095
10098
|
|
|
10096
|
-
// ../shared/src/engine/environment.ts
|
|
10097
|
-
var DESKTOP_NOVNC_PORT = 6080;
|
|
10098
|
-
function getDesktopViewerUrl(publicUrl) {
|
|
10099
|
-
const url2 = new URL(`${publicUrl.replace(/\/$/, "")}/`);
|
|
10100
|
-
url2.searchParams.set("v", "13");
|
|
10101
|
-
return url2.toString();
|
|
10102
|
-
}
|
|
10103
|
-
var DESKTOP_VIEWER_WIDTH = 1920;
|
|
10104
|
-
var DESKTOP_VIEWER_HEIGHT = 1080;
|
|
10105
|
-
|
|
10106
10099
|
// ../node_modules/.bun/zod@4.4.3/node_modules/zod/v4/classic/external.js
|
|
10107
10100
|
var external_exports = {};
|
|
10108
10101
|
__export(external_exports, {
|
|
@@ -27703,7 +27696,7 @@ function generateState() {
|
|
|
27703
27696
|
}
|
|
27704
27697
|
async function loginCommand() {
|
|
27705
27698
|
const state = generateState();
|
|
27706
|
-
return new Promise((
|
|
27699
|
+
return new Promise((resolve2, reject) => {
|
|
27707
27700
|
let authTimeout;
|
|
27708
27701
|
let hasHandledCallback = false;
|
|
27709
27702
|
let lastRedirectUrl = null;
|
|
@@ -27850,7 +27843,7 @@ async function loginCommand() {
|
|
|
27850
27843
|
setImmediate(() => {
|
|
27851
27844
|
server.closeAllConnections?.();
|
|
27852
27845
|
server.close();
|
|
27853
|
-
|
|
27846
|
+
resolve2();
|
|
27854
27847
|
});
|
|
27855
27848
|
} catch (error52) {
|
|
27856
27849
|
const errorUrl = `${WEB_APP_URL}/cli-login/error?message=${encodeURIComponent("Failed to verify authentication.")}`;
|
|
@@ -27956,13 +27949,13 @@ import chalk6 from "chalk";
|
|
|
27956
27949
|
import { spawn } from "child_process";
|
|
27957
27950
|
var SSH_OPTIONS = ["-o", "StrictHostKeyChecking=no", "-o", "UserKnownHostsFile=/dev/null"];
|
|
27958
27951
|
async function connectSSH(token, host, proxyCommand) {
|
|
27959
|
-
return new Promise((
|
|
27952
|
+
return new Promise((resolve2, reject) => {
|
|
27960
27953
|
const sshArgs = proxyCommand ? [...SSH_OPTIONS, "-o", `ProxyCommand=${proxyCommand}`, `${token}@${host}`] : [...SSH_OPTIONS, `${token}@${host}`];
|
|
27961
27954
|
const ssh = spawn("ssh", sshArgs, {
|
|
27962
27955
|
stdio: "inherit"
|
|
27963
27956
|
});
|
|
27964
27957
|
ssh.on("close", () => {
|
|
27965
|
-
|
|
27958
|
+
resolve2();
|
|
27966
27959
|
});
|
|
27967
27960
|
ssh.on("error", reject);
|
|
27968
27961
|
});
|
|
@@ -28471,7 +28464,7 @@ async function exchangeCodeForTokens(code, codeVerifier) {
|
|
|
28471
28464
|
};
|
|
28472
28465
|
}
|
|
28473
28466
|
function startCallbackServer(expectedState, codeVerifier) {
|
|
28474
|
-
return new Promise((
|
|
28467
|
+
return new Promise((resolve2, reject) => {
|
|
28475
28468
|
const server = http2.createServer(async (req, res) => {
|
|
28476
28469
|
try {
|
|
28477
28470
|
if (!req.url) {
|
|
@@ -28514,7 +28507,7 @@ function startCallbackServer(expectedState, codeVerifier) {
|
|
|
28514
28507
|
res.writeHead(302, { "Location": `${WEB_APP_URL2}/codex/oauth/success` });
|
|
28515
28508
|
res.end();
|
|
28516
28509
|
server.close();
|
|
28517
|
-
|
|
28510
|
+
resolve2(tokens);
|
|
28518
28511
|
} catch (tokenError) {
|
|
28519
28512
|
const errorMessage = encodeURIComponent(tokenError instanceof Error ? tokenError.message : "Unknown error");
|
|
28520
28513
|
res.writeHead(302, { "Location": `${WEB_APP_URL2}/codex/oauth/error?message=${errorMessage}` });
|
|
@@ -28548,7 +28541,7 @@ async function runCodexOAuthFlow(totalSteps) {
|
|
|
28548
28541
|
const state = generateState2();
|
|
28549
28542
|
const authUrl = buildAuthorizationUrl(pkce.codeChallenge, state);
|
|
28550
28543
|
const tokensPromise = startCallbackServer(state, pkce.codeVerifier);
|
|
28551
|
-
await new Promise((
|
|
28544
|
+
await new Promise((resolve2) => setTimeout(resolve2, 500));
|
|
28552
28545
|
renderAuthFlowStep(1, totalSteps, "Approve access in your browser");
|
|
28553
28546
|
await openForApproval("auth.openai.com", authUrl);
|
|
28554
28547
|
renderAuthFlowDetail("waiting for sign-in\u2026");
|
|
@@ -28689,10 +28682,10 @@ async function promptForAuthorizationCode() {
|
|
|
28689
28682
|
process.stdin.setRawMode(false);
|
|
28690
28683
|
process.stdin.resume();
|
|
28691
28684
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
28692
|
-
return new Promise((
|
|
28693
|
-
rl.on("close", () =>
|
|
28685
|
+
return new Promise((resolve2) => {
|
|
28686
|
+
rl.on("close", () => resolve2(""));
|
|
28694
28687
|
rl.question(chalk11.gray(" Paste code here: "), (answer) => {
|
|
28695
|
-
|
|
28688
|
+
resolve2(answer.trim());
|
|
28696
28689
|
rl.close();
|
|
28697
28690
|
});
|
|
28698
28691
|
});
|
|
@@ -29297,7 +29290,7 @@ async function replicaReadCommand(id, options) {
|
|
|
29297
29290
|
try {
|
|
29298
29291
|
const params = new URLSearchParams();
|
|
29299
29292
|
if (options.limit) params.set("limit", options.limit);
|
|
29300
|
-
if (options.
|
|
29293
|
+
if (options.beforeEvent) params.set("beforeEvent", options.beforeEvent);
|
|
29301
29294
|
const query = params.toString();
|
|
29302
29295
|
const response = await orgAuthenticatedFetch(
|
|
29303
29296
|
`/v1/replica/${id}/read${query ? "?" + query : ""}`
|
|
@@ -29318,7 +29311,7 @@ Conversation History
|
|
|
29318
29311
|
console.log(chalk14.gray(` Total Events: ${response.total}`));
|
|
29319
29312
|
console.log(chalk14.gray(` Showing: ${response.events.length} events`));
|
|
29320
29313
|
if (response.has_more) {
|
|
29321
|
-
console.log(chalk14.gray(` Has More: yes (
|
|
29314
|
+
console.log(chalk14.gray(` Has More: yes (--before-event ${response.eventsStartIndex} for the previous page)`));
|
|
29322
29315
|
}
|
|
29323
29316
|
console.log();
|
|
29324
29317
|
if (response.events.length === 0) {
|
|
@@ -30911,2871 +30904,8 @@ async function mothershipRelayCommand(options) {
|
|
|
30911
30904
|
console.log("Relayed to the thread's dedicated workspace.");
|
|
30912
30905
|
}
|
|
30913
30906
|
|
|
30914
|
-
// src/commands/computer/index.ts
|
|
30915
|
-
import { spawn as spawn5, spawnSync as spawnSync3 } from "child_process";
|
|
30916
|
-
import { createHash as createHash3 } from "crypto";
|
|
30917
|
-
import { closeSync as closeSync2, copyFileSync as copyFileSync2, existsSync as existsSync3, mkdirSync as mkdirSync5, openSync as openSync2, readFileSync as readFileSync5, readSync, rmSync as rmSync4, writeFileSync as writeFileSync5 } from "fs";
|
|
30918
|
-
import { dirname as dirname3 } from "path";
|
|
30919
|
-
import chalk21 from "chalk";
|
|
30920
|
-
|
|
30921
|
-
// src/commands/computer/desktop.ts
|
|
30922
|
-
import { spawnSync } from "child_process";
|
|
30923
|
-
import { existsSync, mkdirSync as mkdirSync3, readFileSync as readFileSync3 } from "fs";
|
|
30924
|
-
import { dirname, isAbsolute, resolve as resolve2 } from "path";
|
|
30925
|
-
var STATE_DIR = process.env.REPLICAS_DESKTOP_STATE_DIR || "/tmp/replicas-computer";
|
|
30926
|
-
var DEFAULT_DISPLAY = process.env.REPLICAS_DESKTOP_DISPLAY || ":99";
|
|
30927
|
-
var NOVNC_PORT = process.env.REPLICAS_DESKTOP_NOVNC_PORT ? parseInt(process.env.REPLICAS_DESKTOP_NOVNC_PORT, 10) : DESKTOP_NOVNC_PORT;
|
|
30928
|
-
var SERVICES_SCRIPT = "/usr/local/bin/replicas-start-desktop-services";
|
|
30929
|
-
var INPUT_LOCK_FILE = process.env.REPLICAS_DESKTOP_INPUT_LOCK_FILE || `${STATE_DIR}/input.lock`;
|
|
30930
|
-
var CHROME_WRAPPER = "/usr/local/bin/replicas-chrome";
|
|
30931
|
-
var INFO_WAIT_TIMEOUT_MS = 1e4;
|
|
30932
|
-
var INFO_WAIT_INTERVAL_MS = 500;
|
|
30933
|
-
var lastDesktopHealthAt = 0;
|
|
30934
|
-
var CHROME_DEBUG_PORT = (() => {
|
|
30935
|
-
const value = process.env.REPLICAS_DESKTOP_CHROME_DEBUG_PORT;
|
|
30936
|
-
if (!value) return 9222;
|
|
30937
|
-
const port = parseInt(value, 10);
|
|
30938
|
-
if (!Number.isInteger(port) || port <= 0 || port > 65535) {
|
|
30939
|
-
fail(`REPLICAS_DESKTOP_CHROME_DEBUG_PORT must be a valid TCP port, got ${JSON.stringify(value)}`);
|
|
30940
|
-
}
|
|
30941
|
-
return port;
|
|
30942
|
-
})();
|
|
30943
|
-
function fail(msg) {
|
|
30944
|
-
throw new Error(msg);
|
|
30945
|
-
}
|
|
30946
|
-
function getDesktopBridgeStatus() {
|
|
30947
|
-
const r = spawnSync("bash", [SERVICES_SCRIPT, "--status-json"], { stdio: "pipe" });
|
|
30948
|
-
if (r.status !== 0) return null;
|
|
30949
|
-
try {
|
|
30950
|
-
return JSON.parse(r.stdout.toString());
|
|
30951
|
-
} catch {
|
|
30952
|
-
return null;
|
|
30953
|
-
}
|
|
30954
|
-
}
|
|
30955
|
-
function desktopStackHealthy() {
|
|
30956
|
-
const pids = {};
|
|
30957
|
-
for (const name of ["openbox", "tint2", "x11vnc", "novnc"]) {
|
|
30958
|
-
try {
|
|
30959
|
-
const pid = Number.parseInt(readFileSync3(`${STATE_DIR}/${name}.pid`, "utf8").trim(), 10);
|
|
30960
|
-
if (!Number.isFinite(pid)) return false;
|
|
30961
|
-
process.kill(pid, 0);
|
|
30962
|
-
pids[name] = pid;
|
|
30963
|
-
} catch {
|
|
30964
|
-
return false;
|
|
30965
|
-
}
|
|
30966
|
-
}
|
|
30967
|
-
if (spawnSync("xdpyinfo", [], { env: withDisplay(), stdio: "ignore" }).status !== 0) return false;
|
|
30968
|
-
const wm = spawnSync("xprop", ["-root", "_NET_SUPPORTING_WM_CHECK"], { env: withDisplay(), stdio: "pipe" });
|
|
30969
|
-
if (wm.status !== 0 || !wm.stdout?.toString().includes("window id")) return false;
|
|
30970
|
-
const bridge = getDesktopBridgeStatus();
|
|
30971
|
-
return !!bridge?.x11vnc.listenerPids?.includes(pids.x11vnc) && !!bridge.websockify.listenerPids?.includes(pids.novnc);
|
|
30972
|
-
}
|
|
30973
|
-
function ensureServicesRunning() {
|
|
30974
|
-
if (!existsSync(SERVICES_SCRIPT)) {
|
|
30975
|
-
fail(
|
|
30976
|
-
`Desktop services script missing at ${SERVICES_SCRIPT}. The workspace image is out of date \u2014 Xvfb / openbox / x11vnc / websockify must be installed and \`replicas-start-desktop-services\` baked in.`
|
|
30977
|
-
);
|
|
30978
|
-
}
|
|
30979
|
-
if (Date.now() - lastDesktopHealthAt < 1e3) return;
|
|
30980
|
-
if (desktopStackHealthy()) {
|
|
30981
|
-
lastDesktopHealthAt = Date.now();
|
|
30982
|
-
return;
|
|
30983
|
-
}
|
|
30984
|
-
const r = spawnSync("bash", [SERVICES_SCRIPT], { stdio: "pipe" });
|
|
30985
|
-
if (r.status !== 0) {
|
|
30986
|
-
fail(`Failed to start desktop services: ${r.stderr?.toString() || "unknown error"}`);
|
|
30987
|
-
}
|
|
30988
|
-
lastDesktopHealthAt = Date.now();
|
|
30989
|
-
}
|
|
30990
|
-
function withDisplay(env = process.env) {
|
|
30991
|
-
return { ...env, DISPLAY: DEFAULT_DISPLAY };
|
|
30992
|
-
}
|
|
30993
|
-
function runDisplayCmd(bin, args) {
|
|
30994
|
-
ensureServicesRunning();
|
|
30995
|
-
const r = spawnSync(bin, args, { env: withDisplay(), stdio: "pipe" });
|
|
30996
|
-
if (r.status !== 0) {
|
|
30997
|
-
fail(`${bin} ${args.join(" ")} failed: ${r.stderr?.toString().trim() || `exit ${r.status}`}`);
|
|
30998
|
-
}
|
|
30999
|
-
return r.stdout?.toString() ?? "";
|
|
31000
|
-
}
|
|
31001
|
-
function runDesktopInputCmd(args) {
|
|
31002
|
-
mkdirSync3(dirname(INPUT_LOCK_FILE), { recursive: true });
|
|
31003
|
-
return runDisplayCmd("flock", [
|
|
31004
|
-
"--exclusive",
|
|
31005
|
-
"--wait",
|
|
31006
|
-
process.env.REPLICAS_DESKTOP_INPUT_LOCK_WAIT_SECONDS || "60",
|
|
31007
|
-
INPUT_LOCK_FILE,
|
|
31008
|
-
"xdotool",
|
|
31009
|
-
...args
|
|
31010
|
-
]);
|
|
31011
|
-
}
|
|
31012
|
-
function tryDisplayCmd(bin, args) {
|
|
31013
|
-
ensureServicesRunning();
|
|
31014
|
-
const r = spawnSync(bin, args, { env: withDisplay(), stdio: "pipe" });
|
|
31015
|
-
if (r.status !== 0) return null;
|
|
31016
|
-
const out = r.stdout?.toString().trim();
|
|
31017
|
-
return out || null;
|
|
31018
|
-
}
|
|
31019
|
-
function parseCoord(value, label) {
|
|
31020
|
-
if (!/^-?\d+$/.test(value)) fail(`${label} must be an integer (got "${value}")`);
|
|
31021
|
-
const n = Number.parseInt(value, 10);
|
|
31022
|
-
if (!Number.isFinite(n)) fail(`${label} must be an integer (got "${value}")`);
|
|
31023
|
-
return n;
|
|
31024
|
-
}
|
|
31025
|
-
function getDisplayDimensions() {
|
|
31026
|
-
const out = runDisplayCmd("xdpyinfo", []);
|
|
31027
|
-
const match = out.match(/dimensions:\s+(\d+)x(\d+)\s+pixels/);
|
|
31028
|
-
if (!match) fail("could not read display dimensions from xdpyinfo");
|
|
31029
|
-
return { width: Number.parseInt(match[1], 10), height: Number.parseInt(match[2], 10) };
|
|
31030
|
-
}
|
|
31031
|
-
function getMouseLocation() {
|
|
31032
|
-
const out = tryDisplayCmd("xdotool", ["getmouselocation", "--shell"]);
|
|
31033
|
-
if (!out) return null;
|
|
31034
|
-
const values = Object.fromEntries(
|
|
31035
|
-
out.split("\n").map((line) => {
|
|
31036
|
-
const [key, value] = line.split("=");
|
|
31037
|
-
return [key.toLowerCase(), Number.parseInt(value, 10)];
|
|
31038
|
-
})
|
|
31039
|
-
);
|
|
31040
|
-
if (!Number.isFinite(values.x) || !Number.isFinite(values.y)) return null;
|
|
31041
|
-
return {
|
|
31042
|
-
x: values.x,
|
|
31043
|
-
y: values.y,
|
|
31044
|
-
screen: Number.isFinite(values.screen) ? values.screen : 0,
|
|
31045
|
-
window: Number.isFinite(values.window) ? values.window : 0
|
|
31046
|
-
};
|
|
31047
|
-
}
|
|
31048
|
-
function parseScreenCoord(value, label, size) {
|
|
31049
|
-
if (value.endsWith("%")) {
|
|
31050
|
-
const rawPct = value.slice(0, -1);
|
|
31051
|
-
if (!/^(?:\d+(?:\.\d+)?|\.\d+)$/.test(rawPct)) fail(`${label} percent must be a number between 0% and 100% (got "${value}")`);
|
|
31052
|
-
const pct = Number.parseFloat(rawPct);
|
|
31053
|
-
if (!Number.isFinite(pct) || pct < 0 || pct > 100) fail(`${label} percent must be between 0% and 100% (got "${value}")`);
|
|
31054
|
-
return Math.round(pct / 100 * (size - 1));
|
|
31055
|
-
}
|
|
31056
|
-
const n = parseCoord(value, label);
|
|
31057
|
-
if (n < 0 || n >= size) fail(`${label} must be between 0 and ${size - 1} pixels, or 0%-100% (got "${value}")`);
|
|
31058
|
-
return n;
|
|
31059
|
-
}
|
|
31060
|
-
function resolvePath(p) {
|
|
31061
|
-
return isAbsolute(p) ? p : resolve2(process.cwd(), p);
|
|
31062
|
-
}
|
|
31063
|
-
function configuredDesktopDimensions() {
|
|
31064
|
-
const width = parseInt(process.env.REPLICAS_DESKTOP_WIDTH || String(DESKTOP_VIEWER_WIDTH), 10);
|
|
31065
|
-
const height = parseInt(process.env.REPLICAS_DESKTOP_HEIGHT || String(DESKTOP_VIEWER_HEIGHT), 10);
|
|
31066
|
-
if (!Number.isFinite(width) || width <= 0) fail(`REPLICAS_DESKTOP_WIDTH must be a positive integer`);
|
|
31067
|
-
if (!Number.isFinite(height) || height <= 0) fail(`REPLICAS_DESKTOP_HEIGHT must be a positive integer`);
|
|
31068
|
-
return { width, height };
|
|
31069
|
-
}
|
|
31070
|
-
function clamp(n, min, max) {
|
|
31071
|
-
return Math.min(max, Math.max(min, n));
|
|
31072
|
-
}
|
|
31073
|
-
var sleep2 = (ms) => new Promise((resolve3) => setTimeout(resolve3, ms));
|
|
31074
|
-
|
|
31075
|
-
// src/commands/computer/recording.ts
|
|
31076
|
-
import { spawn as spawn4 } from "child_process";
|
|
31077
|
-
import { appendFileSync, existsSync as existsSync2, mkdirSync as mkdirSync4, readFileSync as readFileSync4, rmSync as rmSync3, statSync as statSync2, writeFileSync as writeFileSync4 } from "fs";
|
|
31078
|
-
import { dirname as dirname2 } from "path";
|
|
31079
|
-
|
|
31080
|
-
// src/commands/computer/recording/render.ts
|
|
31081
|
-
import { spawnSync as spawnSync2 } from "child_process";
|
|
31082
|
-
import { copyFileSync, rmSync as rmSync2, writeFileSync as writeFileSync3 } from "fs";
|
|
31083
|
-
|
|
31084
|
-
// src/commands/computer/recording/config.ts
|
|
31085
|
-
var cameraMotion = {
|
|
31086
|
-
clickLeadSeconds: 1.2,
|
|
31087
|
-
clickTailSeconds: 0.5,
|
|
31088
|
-
actionLeadSeconds: 0.9,
|
|
31089
|
-
clickZoom: 1.24,
|
|
31090
|
-
closeClickOutputGapSeconds: 6,
|
|
31091
|
-
clickMoveSeconds: 1.2,
|
|
31092
|
-
returnSeconds: 1,
|
|
31093
|
-
minSegmentSeconds: 0.05
|
|
31094
|
-
};
|
|
31095
|
-
var cursorMotion = {
|
|
31096
|
-
minMoveSeconds: 0.38,
|
|
31097
|
-
maxMoveSeconds: 1.7,
|
|
31098
|
-
speedPxPerSecond: 700,
|
|
31099
|
-
defaultLeadSeconds: 0.16,
|
|
31100
|
-
clickShrinkSeconds: 0.08,
|
|
31101
|
-
clickGrowSeconds: 0.14,
|
|
31102
|
-
clickDwellGapStartSeconds: 0.75,
|
|
31103
|
-
clickDwellFullGapSeconds: 3.2,
|
|
31104
|
-
clickMinDwellSeconds: 0.12,
|
|
31105
|
-
clickMaxDwellSeconds: 1.15,
|
|
31106
|
-
maxDwellGapShare: 0.45,
|
|
31107
|
-
dragLeadSeconds: 0.55,
|
|
31108
|
-
maxKeyframes: 120
|
|
31109
|
-
};
|
|
31110
|
-
var cursorStyle = {
|
|
31111
|
-
size: 30,
|
|
31112
|
-
clickSize: 24
|
|
31113
|
-
};
|
|
31114
|
-
|
|
31115
|
-
// src/commands/computer/recording/ffmpeg-expressions.ts
|
|
31116
|
-
function expressionNumber(n) {
|
|
31117
|
-
return Number.isInteger(n) ? String(n) : n.toFixed(3);
|
|
31118
|
-
}
|
|
31119
|
-
function mixNumber(from, to, progress) {
|
|
31120
|
-
return from + (to - from) * progress;
|
|
31121
|
-
}
|
|
31122
|
-
function easeInOutSmootherNumber(progress) {
|
|
31123
|
-
const p = clamp(progress, 0, 1);
|
|
31124
|
-
return p * p * p * (p * (p * 6 - 15) + 10);
|
|
31125
|
-
}
|
|
31126
|
-
function easeInOutSmootherExpression(progress) {
|
|
31127
|
-
const p = `(${progress})`;
|
|
31128
|
-
return `${p}*${p}*${p}*(${p}*(${p}*6-15)+10)`;
|
|
31129
|
-
}
|
|
31130
|
-
function progressOverTime(timeExpression, seconds) {
|
|
31131
|
-
return easeInOutSmootherExpression(`min(max((${timeExpression})/${seconds.toFixed(3)}\\,0)\\,1)`);
|
|
31132
|
-
}
|
|
31133
|
-
function progressBetween(start, duration3) {
|
|
31134
|
-
return easeInOutSmootherExpression(`min(max((t-${start.toFixed(3)})/${duration3.toFixed(3)}\\,0)\\,1)`);
|
|
31135
|
-
}
|
|
31136
|
-
function mixExpression(from, to, progress) {
|
|
31137
|
-
return `${expressionNumber(from)}+(${expressionNumber(to - from)})*${progress}`;
|
|
31138
|
-
}
|
|
31139
|
-
function cropOrigin(point, zoom, size) {
|
|
31140
|
-
return clamp(point * zoom - size / 2, 0, size * zoom - size);
|
|
31141
|
-
}
|
|
31142
|
-
function easedCropOrigin(fromPoint, toPoint, fromZoom, toZoom, size, progress) {
|
|
31143
|
-
return mixExpression(
|
|
31144
|
-
cropOrigin(fromPoint, fromZoom, size),
|
|
31145
|
-
cropOrigin(toPoint, toZoom, size),
|
|
31146
|
-
progress
|
|
31147
|
-
);
|
|
31148
|
-
}
|
|
31149
|
-
function betweenExpression(start, end) {
|
|
31150
|
-
return `between(t\\,${start.toFixed(3)}\\,${end.toFixed(3)})`;
|
|
31151
|
-
}
|
|
31152
|
-
|
|
31153
|
-
// src/commands/computer/recording/timeline.ts
|
|
31154
|
-
function centerOf(size) {
|
|
31155
|
-
return { x: size.width / 2, y: size.height / 2 };
|
|
31156
|
-
}
|
|
31157
|
-
function actionPoint(action, size) {
|
|
31158
|
-
const x = action.toX ?? action.x ?? size.width / 2;
|
|
31159
|
-
const y = action.toY ?? action.y ?? size.height / 2;
|
|
31160
|
-
return { x: clamp(x, 0, size.width), y: clamp(y, 0, size.height) };
|
|
31161
|
-
}
|
|
31162
|
-
function idleSpeed(duration3) {
|
|
31163
|
-
if (duration3 < 1.2) return 1;
|
|
31164
|
-
return clamp(1 + Math.log1p(duration3 - 0.8) * 1.8, 1.5, 5);
|
|
31165
|
-
}
|
|
31166
|
-
function renderedGapSeconds(duration3) {
|
|
31167
|
-
return duration3 / idleSpeed(duration3);
|
|
31168
|
-
}
|
|
31169
|
-
function timelineActions(actions) {
|
|
31170
|
-
return actions.filter((action) => action.type !== "key" && action.type !== "move");
|
|
31171
|
-
}
|
|
31172
|
-
function actionWindow(action, duration3) {
|
|
31173
|
-
const shouldZoom = action.type === "click";
|
|
31174
|
-
const at = clamp(action.atMs / 1e3, 0, duration3);
|
|
31175
|
-
const start = clamp(at - (shouldZoom ? cameraMotion.clickLeadSeconds : cameraMotion.actionLeadSeconds), 0, duration3);
|
|
31176
|
-
const tail = shouldZoom ? cameraMotion.clickTailSeconds : action.type === "type" ? 1.5 : 0.9;
|
|
31177
|
-
return { start, end: clamp(at + tail, start, duration3), shouldZoom };
|
|
31178
|
-
}
|
|
31179
|
-
function nextClickStart(actions, index, duration3) {
|
|
31180
|
-
const nextClick = actions.slice(index + 1).find((candidate) => candidate.type === "click");
|
|
31181
|
-
return nextClick ? clamp(nextClick.atMs / 1e3 - cameraMotion.clickLeadSeconds, 0, duration3) : null;
|
|
31182
|
-
}
|
|
31183
|
-
function shouldPreserveZoom(nextStart, cursor, lastZoom) {
|
|
31184
|
-
return lastZoom > 1 && nextStart !== null && nextStart > cursor && renderedGapSeconds(nextStart - cursor) <= cameraMotion.closeClickOutputGapSeconds;
|
|
31185
|
-
}
|
|
31186
|
-
function fullFrameSegment(duration3, size) {
|
|
31187
|
-
const center = centerOf(size);
|
|
31188
|
-
return {
|
|
31189
|
-
start: 0,
|
|
31190
|
-
end: duration3,
|
|
31191
|
-
speed: 1,
|
|
31192
|
-
motionSeconds: cameraMotion.returnSeconds,
|
|
31193
|
-
fromZoom: 1,
|
|
31194
|
-
zoom: 1,
|
|
31195
|
-
fromX: center.x,
|
|
31196
|
-
fromY: center.y,
|
|
31197
|
-
...center
|
|
31198
|
-
};
|
|
31199
|
-
}
|
|
31200
|
-
function buildRecordingSegments(actions, duration3, size) {
|
|
31201
|
-
const actionsForTimeline = timelineActions(actions);
|
|
31202
|
-
if (actionsForTimeline.length === 0) return [fullFrameSegment(duration3, size)];
|
|
31203
|
-
const center = centerOf(size);
|
|
31204
|
-
const segments = [];
|
|
31205
|
-
let cursor = 0;
|
|
31206
|
-
let lastPoint = center;
|
|
31207
|
-
let lastZoom = 1;
|
|
31208
|
-
for (let i = 0; i < actionsForTimeline.length; i++) {
|
|
31209
|
-
const action = actionsForTimeline[i];
|
|
31210
|
-
const { start, end, shouldZoom } = actionWindow(action, duration3);
|
|
31211
|
-
const preserveZoom = !shouldZoom && shouldPreserveZoom(nextClickStart(actionsForTimeline, i, duration3), cursor, lastZoom);
|
|
31212
|
-
const point = shouldZoom ? actionPoint(action, size) : preserveZoom ? lastPoint : center;
|
|
31213
|
-
const zoom = shouldZoom ? cameraMotion.clickZoom : preserveZoom ? lastZoom : 1;
|
|
31214
|
-
if (start > cursor + cameraMotion.minSegmentSeconds) {
|
|
31215
|
-
const gap = start - cursor;
|
|
31216
|
-
const speed = idleSpeed(gap);
|
|
31217
|
-
const bridgeZoom = shouldZoom && lastZoom > 1 && renderedGapSeconds(gap) <= cameraMotion.closeClickOutputGapSeconds;
|
|
31218
|
-
segments.push({
|
|
31219
|
-
start: cursor,
|
|
31220
|
-
end: start,
|
|
31221
|
-
speed,
|
|
31222
|
-
motionSeconds: bridgeZoom ? Math.min(cameraMotion.clickMoveSeconds, Math.max(0.18, gap / speed)) : cameraMotion.returnSeconds,
|
|
31223
|
-
fromZoom: lastZoom,
|
|
31224
|
-
zoom: bridgeZoom ? cameraMotion.clickZoom : zoom,
|
|
31225
|
-
fromX: lastPoint.x,
|
|
31226
|
-
fromY: lastPoint.y,
|
|
31227
|
-
...point
|
|
31228
|
-
});
|
|
31229
|
-
lastZoom = bridgeZoom ? cameraMotion.clickZoom : zoom;
|
|
31230
|
-
lastPoint = point;
|
|
31231
|
-
}
|
|
31232
|
-
const actionStart = Math.max(cursor, start);
|
|
31233
|
-
if (end > actionStart + cameraMotion.minSegmentSeconds) {
|
|
31234
|
-
segments.push({
|
|
31235
|
-
start: actionStart,
|
|
31236
|
-
end,
|
|
31237
|
-
speed: 1,
|
|
31238
|
-
motionSeconds: shouldZoom ? cameraMotion.clickMoveSeconds : cameraMotion.returnSeconds,
|
|
31239
|
-
fromZoom: lastZoom,
|
|
31240
|
-
zoom,
|
|
31241
|
-
fromX: lastPoint.x,
|
|
31242
|
-
fromY: lastPoint.y,
|
|
31243
|
-
...point
|
|
31244
|
-
});
|
|
31245
|
-
cursor = end;
|
|
31246
|
-
lastZoom = zoom;
|
|
31247
|
-
}
|
|
31248
|
-
lastPoint = point;
|
|
31249
|
-
}
|
|
31250
|
-
if (duration3 > cursor + cameraMotion.minSegmentSeconds) {
|
|
31251
|
-
segments.push({
|
|
31252
|
-
start: cursor,
|
|
31253
|
-
end: duration3,
|
|
31254
|
-
speed: idleSpeed(duration3 - cursor),
|
|
31255
|
-
motionSeconds: cameraMotion.returnSeconds,
|
|
31256
|
-
fromZoom: lastZoom,
|
|
31257
|
-
zoom: 1,
|
|
31258
|
-
fromX: lastPoint.x,
|
|
31259
|
-
fromY: lastPoint.y,
|
|
31260
|
-
...center
|
|
31261
|
-
});
|
|
31262
|
-
}
|
|
31263
|
-
return segments.filter((segment) => segment.end > segment.start + cameraMotion.minSegmentSeconds);
|
|
31264
|
-
}
|
|
31265
|
-
|
|
31266
|
-
// src/commands/computer/recording/cursor.ts
|
|
31267
|
-
var cursorHotspotRatio = { x: 8 / 32, y: 4 / 32 };
|
|
31268
|
-
function cursorAssets(stamp) {
|
|
31269
|
-
return {
|
|
31270
|
-
path: `/tmp/replicas-cursor-${stamp}.svg`,
|
|
31271
|
-
size: cursorStyle.size
|
|
31272
|
-
};
|
|
31273
|
-
}
|
|
31274
|
-
function clickDwellSeconds(gapSeconds) {
|
|
31275
|
-
if (gapSeconds <= cursorMotion.clickDwellGapStartSeconds) return cursorMotion.defaultLeadSeconds;
|
|
31276
|
-
const progress = (gapSeconds - cursorMotion.clickDwellGapStartSeconds) / (cursorMotion.clickDwellFullGapSeconds - cursorMotion.clickDwellGapStartSeconds);
|
|
31277
|
-
const dwell = mixNumber(
|
|
31278
|
-
cursorMotion.clickMinDwellSeconds,
|
|
31279
|
-
cursorMotion.clickMaxDwellSeconds,
|
|
31280
|
-
easeInOutSmootherNumber(progress)
|
|
31281
|
-
);
|
|
31282
|
-
return Math.min(dwell, gapSeconds * cursorMotion.maxDwellGapShare);
|
|
31283
|
-
}
|
|
31284
|
-
function arrivalLeadSeconds(action, gapSeconds) {
|
|
31285
|
-
return action.type === "click" ? clickDwellSeconds(gapSeconds) : cursorMotion.defaultLeadSeconds;
|
|
31286
|
-
}
|
|
31287
|
-
function cursorKeyframe(action, at, previousAt, size) {
|
|
31288
|
-
if (typeof action.x !== "number" || typeof action.y !== "number") return null;
|
|
31289
|
-
const point = actionPoint(action, size);
|
|
31290
|
-
const lead = arrivalLeadSeconds(action, at - previousAt);
|
|
31291
|
-
return { ...point, at, arriveAt: clamp(at - lead, previousAt, at) };
|
|
31292
|
-
}
|
|
31293
|
-
function dedupeKeyframes(points) {
|
|
31294
|
-
return points.sort((a, b) => a.at - b.at).filter(
|
|
31295
|
-
(point, index, all) => index === 0 || point.at - all[index - 1].at > 0.08 || Math.hypot(point.x - all[index - 1].x, point.y - all[index - 1].y) > 24
|
|
31296
|
-
);
|
|
31297
|
-
}
|
|
31298
|
-
function downsampleKeyframes(points, actions, size) {
|
|
31299
|
-
if (points.length <= cursorMotion.maxKeyframes) return points;
|
|
31300
|
-
const anchors = actions.filter((action) => action.type !== "move").map((action) => actionPoint(action, size));
|
|
31301
|
-
const result = points.filter(
|
|
31302
|
-
(point, index) => index === 0 || index === points.length - 1 || anchors.some((anchor) => Math.hypot(anchor.x - point.x, anchor.y - point.y) < 1)
|
|
31303
|
-
);
|
|
31304
|
-
const step = points.length / Math.max(1, cursorMotion.maxKeyframes - result.length);
|
|
31305
|
-
for (let i = 0; result.length < cursorMotion.maxKeyframes && i < points.length; i += step) {
|
|
31306
|
-
const point = points[Math.floor(i)];
|
|
31307
|
-
if (!result.includes(point)) result.push(point);
|
|
31308
|
-
}
|
|
31309
|
-
return result.sort((a, b) => a.at - b.at);
|
|
31310
|
-
}
|
|
31311
|
-
function cursorKeyframes(actions, duration3, size) {
|
|
31312
|
-
const center = { x: size.width / 2, y: size.height / 2 };
|
|
31313
|
-
const points = [{ at: 0, arriveAt: 0, ...center }];
|
|
31314
|
-
for (const action of actions) {
|
|
31315
|
-
const at = clamp(action.atMs / 1e3, 0, duration3);
|
|
31316
|
-
const previousAt = points[points.length - 1].at;
|
|
31317
|
-
if (action.type === "drag" && typeof action.x === "number" && typeof action.y === "number") {
|
|
31318
|
-
const startAt = clamp(at - cursorMotion.dragLeadSeconds, 0, duration3);
|
|
31319
|
-
points.push({
|
|
31320
|
-
at: startAt,
|
|
31321
|
-
arriveAt: startAt,
|
|
31322
|
-
x: clamp(action.x, 0, size.width),
|
|
31323
|
-
y: clamp(action.y, 0, size.height)
|
|
31324
|
-
});
|
|
31325
|
-
if (typeof action.toX === "number" && typeof action.toY === "number") {
|
|
31326
|
-
points.push({
|
|
31327
|
-
at,
|
|
31328
|
-
arriveAt: at,
|
|
31329
|
-
x: clamp(action.toX, 0, size.width),
|
|
31330
|
-
y: clamp(action.toY, 0, size.height)
|
|
31331
|
-
});
|
|
31332
|
-
}
|
|
31333
|
-
continue;
|
|
31334
|
-
}
|
|
31335
|
-
const point = cursorKeyframe(action, at, previousAt, size);
|
|
31336
|
-
if (point) points.push(point);
|
|
31337
|
-
}
|
|
31338
|
-
return downsampleKeyframes(dedupeKeyframes(points), actions, size);
|
|
31339
|
-
}
|
|
31340
|
-
function cursorAxisExpression(points, axis) {
|
|
31341
|
-
if (points.length === 0) return "0";
|
|
31342
|
-
let expression = expressionNumber(points[points.length - 1][axis]);
|
|
31343
|
-
for (let i = points.length - 2; i >= 0; i--) {
|
|
31344
|
-
const from = points[i];
|
|
31345
|
-
const to = points[i + 1];
|
|
31346
|
-
const arriveAt = clamp(to.arriveAt, from.at, to.at);
|
|
31347
|
-
const moveWindow = Math.max(1e-3, arriveAt - from.at);
|
|
31348
|
-
const distance = Math.hypot(to.x - from.x, to.y - from.y);
|
|
31349
|
-
const moveSeconds = Math.min(
|
|
31350
|
-
moveWindow,
|
|
31351
|
-
Math.max(cursorMotion.minMoveSeconds, Math.min(cursorMotion.maxMoveSeconds, distance / cursorMotion.speedPxPerSecond))
|
|
31352
|
-
);
|
|
31353
|
-
const moveStart = Math.max(from.at, arriveAt - moveSeconds);
|
|
31354
|
-
const duration3 = Math.max(1e-3, arriveAt - moveStart);
|
|
31355
|
-
const interpolated = mixExpression(from[axis], to[axis], progressBetween(moveStart, duration3));
|
|
31356
|
-
expression = `if(lte(t\\,${moveStart.toFixed(3)})\\,${expressionNumber(from[axis])}\\,if(lte(t\\,${to.at.toFixed(3)})\\,${interpolated}\\,${expression}))`;
|
|
31357
|
-
}
|
|
31358
|
-
return expression;
|
|
31359
|
-
}
|
|
31360
|
-
function cursorSizeExpression(actions, normalSize) {
|
|
31361
|
-
let expression = expressionNumber(normalSize);
|
|
31362
|
-
const clicks = actions.filter((action) => action.type === "click").sort((a, b) => b.atMs - a.atMs);
|
|
31363
|
-
for (const action of clicks) {
|
|
31364
|
-
const at = action.atMs / 1e3;
|
|
31365
|
-
const shrinkStart = Math.max(0, at - cursorMotion.clickShrinkSeconds);
|
|
31366
|
-
const shrink = mixExpression(
|
|
31367
|
-
normalSize,
|
|
31368
|
-
cursorStyle.clickSize,
|
|
31369
|
-
progressBetween(shrinkStart, Math.max(1e-3, at - shrinkStart))
|
|
31370
|
-
);
|
|
31371
|
-
const grow = mixExpression(cursorStyle.clickSize, normalSize, progressBetween(at, cursorMotion.clickGrowSeconds));
|
|
31372
|
-
expression = `if(${betweenExpression(shrinkStart, at)}\\,${shrink}\\,if(${betweenExpression(at, at + cursorMotion.clickGrowSeconds)}\\,${grow}\\,${expression}))`;
|
|
31373
|
-
}
|
|
31374
|
-
return expression;
|
|
31375
|
-
}
|
|
31376
|
-
function cursorOverlayPositionExpression(cursor, size, hotspotRatio, overlaySize) {
|
|
31377
|
-
return `max(0\\,min(${expressionNumber(size)}-${overlaySize}\\,(${cursor})-${hotspotRatio.toFixed(6)}*${overlaySize}))`;
|
|
31378
|
-
}
|
|
31379
|
-
function cursorSvg(size) {
|
|
31380
|
-
return `<svg width="${size}" height="${size}" viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg"><path d="M7.7 3.4c-1.45 0-2.62 1.17-2.62 2.62v19.7c0 2.46 3.02 3.65 4.7 1.85l5.06-5.44a5.42 5.42 0 0 1 3.96-1.73h5.82c2.48 0 3.63-3.08 1.76-4.7L9.42 4.04A2.62 2.62 0 0 0 7.7 3.4Z" fill="white" fill-opacity="0.98" stroke="black" stroke-opacity="0.88" stroke-width="2.9" stroke-linejoin="round"/></svg>`;
|
|
31381
|
-
}
|
|
31382
|
-
function buildCursorOverlayFilter(actions, duration3, size, assets, projectCursor = null, inputLabel = "[screen]", outputLabel = "[v]") {
|
|
31383
|
-
const points = cursorKeyframes(actions, duration3, size);
|
|
31384
|
-
const rawCursorX = cursorAxisExpression(points, "x");
|
|
31385
|
-
const rawCursorY = cursorAxisExpression(points, "y");
|
|
31386
|
-
const cursorX = projectCursor ? projectCursor.x(rawCursorX) : rawCursorX;
|
|
31387
|
-
const cursorY = projectCursor ? projectCursor.y(rawCursorY) : rawCursorY;
|
|
31388
|
-
const cursorSize = cursorSizeExpression(actions, assets.size);
|
|
31389
|
-
const cursorOverlayX = cursorOverlayPositionExpression(cursorX, size.width, cursorHotspotRatio.x, "overlay_w");
|
|
31390
|
-
const cursorOverlayY = cursorOverlayPositionExpression(cursorY, size.height, cursorHotspotRatio.y, "overlay_h");
|
|
31391
|
-
return `[1:v]scale=w='${cursorSize}':h='${cursorSize}':eval=frame[cursor];${inputLabel}[cursor]overlay=x='${cursorOverlayX}':y='${cursorOverlayY}':eof_action=repeat:shortest=1:eval=frame${outputLabel}`;
|
|
31392
|
-
}
|
|
31393
|
-
|
|
31394
|
-
// src/commands/computer/recording/render.ts
|
|
31395
|
-
function videoDuration(path6) {
|
|
31396
|
-
const r = spawnSync2("ffprobe", [
|
|
31397
|
-
"-v",
|
|
31398
|
-
"error",
|
|
31399
|
-
"-show_entries",
|
|
31400
|
-
"format=duration",
|
|
31401
|
-
"-of",
|
|
31402
|
-
"default=noprint_wrappers=1:nokey=1",
|
|
31403
|
-
path6
|
|
31404
|
-
], { stdio: "pipe" });
|
|
31405
|
-
if (r.status !== 0) fail(`ffprobe failed: ${r.stderr?.toString().trim() || `exit ${r.status}`}`);
|
|
31406
|
-
const duration3 = Number.parseFloat(r.stdout?.toString().trim() ?? "");
|
|
31407
|
-
if (!Number.isFinite(duration3) || duration3 <= 0) fail(`could not read recording duration for ${path6}`);
|
|
31408
|
-
return duration3;
|
|
31409
|
-
}
|
|
31410
|
-
function renderedSegmentSpans(segments) {
|
|
31411
|
-
let outputStart = 0;
|
|
31412
|
-
return segments.map((segment) => {
|
|
31413
|
-
const outputEnd = outputStart + (segment.end - segment.start) / segment.speed;
|
|
31414
|
-
const span = {
|
|
31415
|
-
rawStart: segment.start,
|
|
31416
|
-
rawEnd: segment.end,
|
|
31417
|
-
outputStart,
|
|
31418
|
-
outputEnd,
|
|
31419
|
-
speed: segment.speed
|
|
31420
|
-
};
|
|
31421
|
-
outputStart = outputEnd;
|
|
31422
|
-
return span;
|
|
31423
|
-
});
|
|
31424
|
-
}
|
|
31425
|
-
function outputTimeForRawTime(spans, rawTime) {
|
|
31426
|
-
const span = spans.find((candidate) => rawTime >= candidate.rawStart && rawTime <= candidate.rawEnd);
|
|
31427
|
-
if (span) return span.outputStart + (rawTime - span.rawStart) / span.speed;
|
|
31428
|
-
if (spans.length === 0) return rawTime;
|
|
31429
|
-
if (rawTime <= spans[0].rawStart) return spans[0].outputStart;
|
|
31430
|
-
const lastSpan = spans[spans.length - 1];
|
|
31431
|
-
if (rawTime >= lastSpan.rawEnd) return lastSpan.outputEnd;
|
|
31432
|
-
return spans.find((candidate) => rawTime < candidate.rawStart)?.outputStart ?? lastSpan.outputEnd;
|
|
31433
|
-
}
|
|
31434
|
-
function actionsOnRenderedTimeline(actions, spans) {
|
|
31435
|
-
return actions.map((action) => ({
|
|
31436
|
-
...action,
|
|
31437
|
-
atMs: outputTimeForRawTime(spans, action.atMs / 1e3) * 1e3
|
|
31438
|
-
}));
|
|
31439
|
-
}
|
|
31440
|
-
function segmentMotionSeconds(segment) {
|
|
31441
|
-
return Math.min(segment.motionSeconds, Math.max(0.12, (segment.end - segment.start) / segment.speed));
|
|
31442
|
-
}
|
|
31443
|
-
function scaledFrameClampExpression(value, zoom, outputSize) {
|
|
31444
|
-
return `max(0\\,min(${outputSize}*(${zoom})-${outputSize}\\,${value}))`;
|
|
31445
|
-
}
|
|
31446
|
-
function cameraTransformExpressions(segment, size, timeExpression) {
|
|
31447
|
-
const progress = progressOverTime(timeExpression, segmentMotionSeconds(segment));
|
|
31448
|
-
const zoom = mixExpression(segment.fromZoom, segment.zoom, progress);
|
|
31449
|
-
return {
|
|
31450
|
-
zoom,
|
|
31451
|
-
cropX: scaledFrameClampExpression(
|
|
31452
|
-
easedCropOrigin(segment.fromX, segment.x, segment.fromZoom, segment.zoom, size.width, progress),
|
|
31453
|
-
zoom,
|
|
31454
|
-
size.width
|
|
31455
|
-
),
|
|
31456
|
-
cropY: scaledFrameClampExpression(
|
|
31457
|
-
easedCropOrigin(segment.fromY, segment.y, segment.fromZoom, segment.zoom, size.height, progress),
|
|
31458
|
-
zoom,
|
|
31459
|
-
size.height
|
|
31460
|
-
)
|
|
31461
|
-
};
|
|
31462
|
-
}
|
|
31463
|
-
function projectedCursorExpression(rawCursor, axis, segments, spans, size) {
|
|
31464
|
-
let expression = rawCursor;
|
|
31465
|
-
for (let i = segments.length - 1; i >= 0; i--) {
|
|
31466
|
-
const span = spans[i];
|
|
31467
|
-
const camera = cameraTransformExpressions(segments[i], size, `t-${span.outputStart.toFixed(3)}`);
|
|
31468
|
-
const crop = axis === "x" ? camera.cropX : camera.cropY;
|
|
31469
|
-
const projected = `(${rawCursor})*(${camera.zoom})-(${crop})`;
|
|
31470
|
-
expression = `if(${betweenExpression(span.outputStart, span.outputEnd)}\\,${projected}\\,${expression})`;
|
|
31471
|
-
}
|
|
31472
|
-
return expression;
|
|
31473
|
-
}
|
|
31474
|
-
function renderRecording(rawPath, target, actions, fps, size) {
|
|
31475
|
-
if (actions.length === 0) {
|
|
31476
|
-
copyFileSync(rawPath, target);
|
|
31477
|
-
return;
|
|
31478
|
-
}
|
|
31479
|
-
const duration3 = videoDuration(rawPath);
|
|
31480
|
-
const segments = buildRecordingSegments(actions, duration3, size);
|
|
31481
|
-
if (segments.length === 0) {
|
|
31482
|
-
copyFileSync(rawPath, target);
|
|
31483
|
-
return;
|
|
31484
|
-
}
|
|
31485
|
-
const stamp = `${process.pid}-${Date.now()}`;
|
|
31486
|
-
const cursor = cursorAssets(stamp);
|
|
31487
|
-
writeFileSync3(cursor.path, cursorSvg(cursor.size));
|
|
31488
|
-
const spans = renderedSegmentSpans(segments);
|
|
31489
|
-
const renderedDuration = spans.length ? spans[spans.length - 1].outputEnd : duration3;
|
|
31490
|
-
const renderedActions = actionsOnRenderedTimeline(actions, spans);
|
|
31491
|
-
const cursorFilter = buildCursorOverlayFilter(
|
|
31492
|
-
renderedActions,
|
|
31493
|
-
renderedDuration,
|
|
31494
|
-
size,
|
|
31495
|
-
cursor,
|
|
31496
|
-
{
|
|
31497
|
-
x: (cursorX) => projectedCursorExpression(cursorX, "x", segments, spans, size),
|
|
31498
|
-
y: (cursorY) => projectedCursorExpression(cursorY, "y", segments, spans, size)
|
|
31499
|
-
}
|
|
31500
|
-
);
|
|
31501
|
-
const screenLabels = segments.map((_, index) => `[screen${index}]`).join("");
|
|
31502
|
-
const screenSplitFilter = `[0:v]split=${segments.length}${screenLabels}`;
|
|
31503
|
-
const filters = segments.map((segment, index) => {
|
|
31504
|
-
const { zoom, cropX, cropY } = cameraTransformExpressions(segment, size, "t");
|
|
31505
|
-
return `[screen${index}]trim=start=${segment.start.toFixed(3)}:end=${segment.end.toFixed(3)},setpts=(PTS-STARTPTS)/${segment.speed.toFixed(3)},scale=w=${size.width}*(${zoom}):h=${size.height}*(${zoom}):eval=frame,crop=${size.width}:${size.height}:x=${cropX}:y=${cropY},setsar=1[v${index}]`;
|
|
31506
|
-
});
|
|
31507
|
-
const concatInputs = segments.map((_, index) => `[v${index}]`).join("");
|
|
31508
|
-
const filter = `${screenSplitFilter};${filters.join(";")};${concatInputs}concat=n=${segments.length}:v=1:a=0[screen];${cursorFilter}`;
|
|
31509
|
-
const filterPath = `/tmp/replicas-recording-filter-${stamp}.ffgraph`;
|
|
31510
|
-
writeFileSync3(filterPath, filter);
|
|
31511
|
-
try {
|
|
31512
|
-
const r = spawnSync2("ffmpeg", [
|
|
31513
|
-
"-y",
|
|
31514
|
-
"-hide_banner",
|
|
31515
|
-
"-loglevel",
|
|
31516
|
-
"warning",
|
|
31517
|
-
"-i",
|
|
31518
|
-
rawPath,
|
|
31519
|
-
"-loop",
|
|
31520
|
-
"1",
|
|
31521
|
-
"-i",
|
|
31522
|
-
cursor.path,
|
|
31523
|
-
"-filter_complex_script",
|
|
31524
|
-
filterPath,
|
|
31525
|
-
"-map",
|
|
31526
|
-
"[v]",
|
|
31527
|
-
"-r",
|
|
31528
|
-
String(fps),
|
|
31529
|
-
"-c:v",
|
|
31530
|
-
"libx264",
|
|
31531
|
-
"-preset",
|
|
31532
|
-
"veryfast",
|
|
31533
|
-
"-crf",
|
|
31534
|
-
"23",
|
|
31535
|
-
"-pix_fmt",
|
|
31536
|
-
"yuv420p",
|
|
31537
|
-
"-movflags",
|
|
31538
|
-
"+faststart",
|
|
31539
|
-
target
|
|
31540
|
-
], { stdio: "pipe", maxBuffer: 20 * 1024 * 1024 });
|
|
31541
|
-
if (r.status !== 0) {
|
|
31542
|
-
fail(`recording post-processing failed: ${r.error?.message || r.stderr?.toString().trim() || `exit ${r.status}`}`);
|
|
31543
|
-
}
|
|
31544
|
-
} finally {
|
|
31545
|
-
rmSync2(filterPath, { force: true });
|
|
31546
|
-
rmSync2(cursor.path, { force: true });
|
|
31547
|
-
}
|
|
31548
|
-
}
|
|
31549
|
-
|
|
31550
|
-
// src/commands/computer/recording.ts
|
|
31551
|
-
var RECORD_PID_FILE = `${STATE_DIR}/ffmpeg.pid`;
|
|
31552
|
-
var RECORD_PATH_FILE = `${STATE_DIR}/recording-path.txt`;
|
|
31553
|
-
var RECORD_RAW_PATH_FILE = `${STATE_DIR}/recording-raw-path.txt`;
|
|
31554
|
-
var RECORD_STARTED_AT_FILE = `${STATE_DIR}/recording-started-at.txt`;
|
|
31555
|
-
var RECORD_FPS_FILE = `${STATE_DIR}/recording-fps.txt`;
|
|
31556
|
-
var RECORD_DIMENSIONS_FILE = `${STATE_DIR}/recording-dimensions.json`;
|
|
31557
|
-
var RECORD_ACTIONS_FILE = `${STATE_DIR}/recording-actions.jsonl`;
|
|
31558
|
-
var RECORD_STATE_FILES = [
|
|
31559
|
-
RECORD_PID_FILE,
|
|
31560
|
-
RECORD_PATH_FILE,
|
|
31561
|
-
RECORD_RAW_PATH_FILE,
|
|
31562
|
-
RECORD_STARTED_AT_FILE,
|
|
31563
|
-
RECORD_FPS_FILE,
|
|
31564
|
-
RECORD_DIMENSIONS_FILE,
|
|
31565
|
-
RECORD_ACTIONS_FILE
|
|
31566
|
-
];
|
|
31567
|
-
function clearRecordingState() {
|
|
31568
|
-
for (const file2 of RECORD_STATE_FILES) rmSync3(file2, { force: true });
|
|
31569
|
-
}
|
|
31570
|
-
function recordingStartedAt() {
|
|
31571
|
-
if (!existsSync2(RECORD_STARTED_AT_FILE)) return null;
|
|
31572
|
-
const startedAt = Number.parseInt(readFileSync4(RECORD_STARTED_AT_FILE, "utf8").trim(), 10);
|
|
31573
|
-
return Number.isFinite(startedAt) ? startedAt : null;
|
|
31574
|
-
}
|
|
31575
|
-
function logRecordingAction(action) {
|
|
31576
|
-
const startedAt = recordingStartedAt();
|
|
31577
|
-
if (!startedAt) return;
|
|
31578
|
-
const atMs = Date.now() - startedAt;
|
|
31579
|
-
appendFileSync(RECORD_ACTIONS_FILE, `${JSON.stringify({ ...action, atMs })}
|
|
31580
|
-
`);
|
|
31581
|
-
}
|
|
31582
|
-
function readRecordingDimensions() {
|
|
31583
|
-
if (!existsSync2(RECORD_DIMENSIONS_FILE)) return configuredDesktopDimensions();
|
|
31584
|
-
try {
|
|
31585
|
-
const dimensions = JSON.parse(readFileSync4(RECORD_DIMENSIONS_FILE, "utf8"));
|
|
31586
|
-
const width = dimensions?.width;
|
|
31587
|
-
const height = dimensions?.height;
|
|
31588
|
-
if (typeof width === "number" && Number.isFinite(width) && width > 0 && typeof height === "number" && Number.isFinite(height) && height > 0) {
|
|
31589
|
-
return { width, height };
|
|
31590
|
-
}
|
|
31591
|
-
} catch {
|
|
31592
|
-
}
|
|
31593
|
-
fail("invalid recording dimensions state");
|
|
31594
|
-
}
|
|
31595
|
-
function isRecordingActionType(value) {
|
|
31596
|
-
return value === "click" || value === "drag" || value === "key" || value === "move" || value === "scroll" || value === "type";
|
|
31597
|
-
}
|
|
31598
|
-
function isOptionalNumber(value) {
|
|
31599
|
-
return value === void 0 || typeof value === "number";
|
|
31600
|
-
}
|
|
31601
|
-
function readRecordingActions() {
|
|
31602
|
-
if (!existsSync2(RECORD_ACTIONS_FILE)) return [];
|
|
31603
|
-
return readFileSync4(RECORD_ACTIONS_FILE, "utf8").split("\n").filter(Boolean).flatMap((line) => {
|
|
31604
|
-
try {
|
|
31605
|
-
const value = JSON.parse(line);
|
|
31606
|
-
if (typeof value !== "object" || value === null) return [];
|
|
31607
|
-
const type = "type" in value ? value.type : void 0;
|
|
31608
|
-
const atMs = "atMs" in value ? value.atMs : void 0;
|
|
31609
|
-
const x = "x" in value ? value.x : void 0;
|
|
31610
|
-
const y = "y" in value ? value.y : void 0;
|
|
31611
|
-
const toX = "toX" in value ? value.toX : void 0;
|
|
31612
|
-
const toY = "toY" in value ? value.toY : void 0;
|
|
31613
|
-
if (!isRecordingActionType(type) || typeof atMs !== "number" || !Number.isFinite(atMs) || !isOptionalNumber(x) || !isOptionalNumber(y) || !isOptionalNumber(toX) || !isOptionalNumber(toY)) {
|
|
31614
|
-
return [];
|
|
31615
|
-
}
|
|
31616
|
-
return [{ type, atMs, x, y, toX, toY }];
|
|
31617
|
-
} catch {
|
|
31618
|
-
return [];
|
|
31619
|
-
}
|
|
31620
|
-
}).sort((a, b) => a.atMs - b.atMs);
|
|
31621
|
-
}
|
|
31622
|
-
async function computerRecordStartCommand(path6, options) {
|
|
31623
|
-
ensureServicesRunning();
|
|
31624
|
-
if (existsSync2(RECORD_PID_FILE)) {
|
|
31625
|
-
const pid = parseInt(readFileSync4(RECORD_PID_FILE, "utf8").trim(), 10);
|
|
31626
|
-
if (Number.isFinite(pid)) {
|
|
31627
|
-
let alive2 = false;
|
|
31628
|
-
try {
|
|
31629
|
-
process.kill(pid, 0);
|
|
31630
|
-
alive2 = true;
|
|
31631
|
-
} catch {
|
|
31632
|
-
}
|
|
31633
|
-
if (alive2) fail(`recording already in progress (pid ${pid}). run \`replicas computer record stop\` first.`);
|
|
31634
|
-
}
|
|
31635
|
-
}
|
|
31636
|
-
const target = resolvePath(path6);
|
|
31637
|
-
mkdirSync4(dirname2(target), { recursive: true });
|
|
31638
|
-
const fps = options.fps ? parseCoord(options.fps, "--fps") : 60;
|
|
31639
|
-
const { width, height } = configuredDesktopDimensions();
|
|
31640
|
-
mkdirSync4(STATE_DIR, { recursive: true });
|
|
31641
|
-
const rawTarget = `${target}.raw-${Date.now()}.mp4`;
|
|
31642
|
-
rmSync3(RECORD_ACTIONS_FILE, { force: true });
|
|
31643
|
-
const child = spawn4("ffmpeg", [
|
|
31644
|
-
"-y",
|
|
31645
|
-
"-hide_banner",
|
|
31646
|
-
"-loglevel",
|
|
31647
|
-
"warning",
|
|
31648
|
-
"-f",
|
|
31649
|
-
"x11grab",
|
|
31650
|
-
"-framerate",
|
|
31651
|
-
String(fps),
|
|
31652
|
-
"-video_size",
|
|
31653
|
-
`${width}x${height}`,
|
|
31654
|
-
"-draw_mouse",
|
|
31655
|
-
"0",
|
|
31656
|
-
"-i",
|
|
31657
|
-
DEFAULT_DISPLAY,
|
|
31658
|
-
"-c:v",
|
|
31659
|
-
"libx264",
|
|
31660
|
-
"-preset",
|
|
31661
|
-
"ultrafast",
|
|
31662
|
-
"-tune",
|
|
31663
|
-
"zerolatency",
|
|
31664
|
-
"-crf",
|
|
31665
|
-
"23",
|
|
31666
|
-
"-pix_fmt",
|
|
31667
|
-
"yuv420p",
|
|
31668
|
-
"-movflags",
|
|
31669
|
-
"+faststart+frag_keyframe+empty_moov",
|
|
31670
|
-
rawTarget
|
|
31671
|
-
], { detached: true, stdio: "ignore" });
|
|
31672
|
-
child.unref();
|
|
31673
|
-
if (!child.pid) fail("failed to launch ffmpeg");
|
|
31674
|
-
writeFileSync4(RECORD_PID_FILE, String(child.pid));
|
|
31675
|
-
writeFileSync4(RECORD_PATH_FILE, target);
|
|
31676
|
-
writeFileSync4(RECORD_RAW_PATH_FILE, rawTarget);
|
|
31677
|
-
writeFileSync4(RECORD_STARTED_AT_FILE, String(Date.now()));
|
|
31678
|
-
writeFileSync4(RECORD_FPS_FILE, String(fps));
|
|
31679
|
-
writeFileSync4(RECORD_DIMENSIONS_FILE, JSON.stringify({ width, height }));
|
|
31680
|
-
const startedAt = Date.now();
|
|
31681
|
-
while (Date.now() - startedAt < 5e3) {
|
|
31682
|
-
try {
|
|
31683
|
-
process.kill(child.pid, 0);
|
|
31684
|
-
if (existsSync2(rawTarget) && statSync2(rawTarget).size > 0) {
|
|
31685
|
-
console.log(`${target} (recording ready in ${Date.now() - startedAt}ms)`);
|
|
31686
|
-
return;
|
|
31687
|
-
}
|
|
31688
|
-
} catch {
|
|
31689
|
-
break;
|
|
31690
|
-
}
|
|
31691
|
-
await sleep2(100);
|
|
31692
|
-
}
|
|
31693
|
-
let alive = false;
|
|
31694
|
-
try {
|
|
31695
|
-
process.kill(child.pid, 0);
|
|
31696
|
-
alive = true;
|
|
31697
|
-
} catch {
|
|
31698
|
-
}
|
|
31699
|
-
if (alive) fail("ffmpeg is running but did not produce recording output within 5 seconds; run `replicas computer record stop` to finalize or retry");
|
|
31700
|
-
clearRecordingState();
|
|
31701
|
-
rmSync3(rawTarget, { force: true });
|
|
31702
|
-
fail("ffmpeg exited before screen recording became ready");
|
|
31703
|
-
}
|
|
31704
|
-
async function computerRecordStopCommand() {
|
|
31705
|
-
if (!existsSync2(RECORD_PID_FILE) && !existsSync2(RECORD_PATH_FILE)) fail("no recording in progress");
|
|
31706
|
-
if (existsSync2(RECORD_PID_FILE)) {
|
|
31707
|
-
const pid = parseInt(readFileSync4(RECORD_PID_FILE, "utf8").trim(), 10);
|
|
31708
|
-
if (!Number.isFinite(pid)) fail("invalid recording pidfile");
|
|
31709
|
-
try {
|
|
31710
|
-
process.kill(pid, "SIGINT");
|
|
31711
|
-
} catch {
|
|
31712
|
-
}
|
|
31713
|
-
let alive = true;
|
|
31714
|
-
for (let i = 0; i < 150; i++) {
|
|
31715
|
-
try {
|
|
31716
|
-
process.kill(pid, 0);
|
|
31717
|
-
} catch {
|
|
31718
|
-
alive = false;
|
|
31719
|
-
break;
|
|
31720
|
-
}
|
|
31721
|
-
await sleep2(200);
|
|
31722
|
-
}
|
|
31723
|
-
if (alive) fail(`ffmpeg did not finalize recording within 30 seconds (pid ${pid})`);
|
|
31724
|
-
}
|
|
31725
|
-
if (existsSync2(RECORD_PATH_FILE)) {
|
|
31726
|
-
const target = readFileSync4(RECORD_PATH_FILE, "utf8").trim();
|
|
31727
|
-
const rawPath = existsSync2(RECORD_RAW_PATH_FILE) ? readFileSync4(RECORD_RAW_PATH_FILE, "utf8").trim() : target;
|
|
31728
|
-
const fps = existsSync2(RECORD_FPS_FILE) ? parseInt(readFileSync4(RECORD_FPS_FILE, "utf8").trim(), 10) : 60;
|
|
31729
|
-
const size = readRecordingDimensions();
|
|
31730
|
-
const actions = readRecordingActions();
|
|
31731
|
-
if (rawPath !== target) {
|
|
31732
|
-
renderRecording(rawPath, target, actions, Number.isFinite(fps) ? fps : 60, size);
|
|
31733
|
-
rmSync3(rawPath, { force: true });
|
|
31734
|
-
}
|
|
31735
|
-
console.log(target);
|
|
31736
|
-
}
|
|
31737
|
-
clearRecordingState();
|
|
31738
|
-
}
|
|
31739
|
-
|
|
31740
|
-
// src/commands/computer/index.ts
|
|
31741
|
-
function bridgeStatus(details, includeBacklog = false, rootsOnly = false) {
|
|
31742
|
-
const pids = rootsOnly ? details.rootPids ?? [] : details.listenerPids ?? [];
|
|
31743
|
-
const listenerCount = rootsOnly ? details.rootListeners ?? pids.length : details.listeners ?? pids.length;
|
|
31744
|
-
const listenerLabel = rootsOnly ? "root listener" : "listener";
|
|
31745
|
-
const parts = [`${listenerCount} ${listenerLabel}${listenerCount === 1 ? "" : "s"} on ${details.port}`];
|
|
31746
|
-
if (includeBacklog) parts.push(`backlog ${details.backlog ?? 0}`);
|
|
31747
|
-
if (pids.length > 0) parts.push(`pid${pids.length === 1 ? "" : "s"} ${pids.join(",")}`);
|
|
31748
|
-
return ` (${parts.join(", ")})`;
|
|
31749
|
-
}
|
|
31750
|
-
async function lookupDesktopViewerUrl() {
|
|
31751
|
-
try {
|
|
31752
|
-
const list = await listAgentPreviews();
|
|
31753
|
-
const preview2 = list.previews.find((p) => p.port === NOVNC_PORT);
|
|
31754
|
-
return preview2 ? getDesktopViewerUrl(preview2.publicUrl) : null;
|
|
31755
|
-
} catch {
|
|
31756
|
-
return null;
|
|
31757
|
-
}
|
|
31758
|
-
}
|
|
31759
|
-
async function waitForDesktopViewerUrl(timeoutMs) {
|
|
31760
|
-
const deadline = Date.now() + timeoutMs;
|
|
31761
|
-
while (Date.now() < deadline) {
|
|
31762
|
-
const url2 = await lookupDesktopViewerUrl();
|
|
31763
|
-
if (url2) return url2;
|
|
31764
|
-
await new Promise((resolve3) => setTimeout(resolve3, INFO_WAIT_INTERVAL_MS));
|
|
31765
|
-
}
|
|
31766
|
-
return await lookupDesktopViewerUrl();
|
|
31767
|
-
}
|
|
31768
|
-
async function computerInfoCommand() {
|
|
31769
|
-
const viewerUrl = await waitForDesktopViewerUrl(INFO_WAIT_TIMEOUT_MS);
|
|
31770
|
-
if (!viewerUrl) {
|
|
31771
|
-
fail(
|
|
31772
|
-
`Desktop preview for port ${NOVNC_PORT} is not registered. The engine registers it at startup \u2014 check /tmp/replicas-desktop-stack.log and engine logs for errors.`
|
|
31773
|
-
);
|
|
31774
|
-
}
|
|
31775
|
-
console.log(viewerUrl);
|
|
31776
|
-
console.error(chalk21.dim(`Share this URL with the user to let them watch the desktop live.`));
|
|
31777
|
-
}
|
|
31778
|
-
async function computerStatusCommand() {
|
|
31779
|
-
ensureServicesRunning();
|
|
31780
|
-
const bridge = getDesktopBridgeStatus();
|
|
31781
|
-
const procs = ["Xvfb", "openbox", "tint2", "x11vnc", "websockify"];
|
|
31782
|
-
for (const p of procs) {
|
|
31783
|
-
const r = spawnSync3("pgrep", ["-af", p], { stdio: "pipe" });
|
|
31784
|
-
const running = r.status === 0 && !!r.stdout?.toString().trim();
|
|
31785
|
-
const suffix = p === "x11vnc" && bridge ? bridgeStatus(bridge.x11vnc, true) : p === "websockify" && bridge ? bridgeStatus(bridge.websockify, false, true) : "";
|
|
31786
|
-
console.log(` ${running ? chalk21.green("\u25CF") : chalk21.red("\u25CB")} ${p}${suffix}`);
|
|
31787
|
-
}
|
|
31788
|
-
const viewerUrl = await lookupDesktopViewerUrl();
|
|
31789
|
-
if (viewerUrl) {
|
|
31790
|
-
console.log(` ${chalk21.cyan("preview")}: ${viewerUrl}`);
|
|
31791
|
-
} else {
|
|
31792
|
-
console.log(` ${chalk21.dim("preview: not yet registered (engine registers it at startup)")}`);
|
|
31793
|
-
}
|
|
31794
|
-
}
|
|
31795
|
-
var PNG_SIGNATURE = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
|
|
31796
|
-
function readPngDimensions(filePath) {
|
|
31797
|
-
const fd = openSync2(filePath, "r");
|
|
31798
|
-
try {
|
|
31799
|
-
const buf = Buffer.alloc(24);
|
|
31800
|
-
const bytesRead = readSync(fd, buf, 0, 24, 0);
|
|
31801
|
-
if (bytesRead < 24 || !buf.subarray(0, 8).equals(PNG_SIGNATURE)) {
|
|
31802
|
-
fail(`${filePath} is not a valid PNG (read ${bytesRead}/24 bytes)`);
|
|
31803
|
-
}
|
|
31804
|
-
return { width: buf.readUInt32BE(16), height: buf.readUInt32BE(20) };
|
|
31805
|
-
} finally {
|
|
31806
|
-
closeSync2(fd);
|
|
31807
|
-
}
|
|
31808
|
-
}
|
|
31809
|
-
function brandSvgPath() {
|
|
31810
|
-
const dir = process.env.REPLICAS_DESKTOP_TEMPLATES || "/usr/local/share/replicas/desktop";
|
|
31811
|
-
return `${dir}/brand-wallpaper.svg`;
|
|
31812
|
-
}
|
|
31813
|
-
function loadBrandSvg(canvasW, canvasH) {
|
|
31814
|
-
const path6 = brandSvgPath();
|
|
31815
|
-
if (!existsSync3(path6)) {
|
|
31816
|
-
fail(
|
|
31817
|
-
`Brand wallpaper SVG missing at ${path6}. The workspace image is out of date \u2014 \`desktop/brand-wallpaper.svg\` must be installed at $REPLICAS_DESKTOP_TEMPLATES.`
|
|
31818
|
-
);
|
|
31819
|
-
}
|
|
31820
|
-
return readFileSync5(path6, "utf8").replace(/(<svg[^>]*\s)width="\d+"/, `$1width="${canvasW}"`).replace(/(<svg[^>]*\s)height="\d+"/, `$1height="${canvasH}"`);
|
|
31821
|
-
}
|
|
31822
|
-
var BRAND_PAD_FRACTION = 0.06;
|
|
31823
|
-
var SCREENSHOT_CORNER_FRACTION = 0.022;
|
|
31824
|
-
var SHADOW_SIGMA_FRACTION = 0.022;
|
|
31825
|
-
var SHADOW_OFFSET_Y_FRACTION = 0.013;
|
|
31826
|
-
var SHADOW_ALPHA = 0.6;
|
|
31827
|
-
var SCREENSHOT_MASK_TEMPLATE = `<svg width="__W__" height="__H__" xmlns="http://www.w3.org/2000/svg"><rect width="__W__" height="__H__" rx="__R__" ry="__R__" fill="white"/></svg>`;
|
|
31828
|
-
var SHADOW_MASK_TEMPLATE = `<svg width="__SW__" height="__SH__" xmlns="http://www.w3.org/2000/svg"><rect x="__M__" y="__M__" width="__W__" height="__H__" rx="__R__" ry="__R__" fill="white"/></svg>`;
|
|
31829
|
-
function parseGridSize(value) {
|
|
31830
|
-
if (value === void 0 || value === false) return null;
|
|
31831
|
-
const size = value === true ? 100 : parseCoord(value, "--grid");
|
|
31832
|
-
if (size < 25 || size > 500) fail("--grid must be between 25 and 500 pixels");
|
|
31833
|
-
return size;
|
|
31834
|
-
}
|
|
31835
|
-
function buildGridSvg(width, height, size) {
|
|
31836
|
-
const lines = [];
|
|
31837
|
-
const labels = [];
|
|
31838
|
-
for (let x = 0; x <= width; x += size) {
|
|
31839
|
-
const strokeWidth = x % (size * 5) === 0 ? 2 : 1;
|
|
31840
|
-
lines.push(`<line x1="${x}" y1="0" x2="${x}" y2="${height}" stroke="#00e5ff" stroke-opacity="0.42" stroke-width="${strokeWidth}"/>`);
|
|
31841
|
-
if (x > 0 && x < width) labels.push(`<text x="${x + 4}" y="18">${x}</text>`);
|
|
31842
|
-
}
|
|
31843
|
-
for (let y = 0; y <= height; y += size) {
|
|
31844
|
-
const strokeWidth = y % (size * 5) === 0 ? 2 : 1;
|
|
31845
|
-
lines.push(`<line x1="0" y1="${y}" x2="${width}" y2="${y}" stroke="#00e5ff" stroke-opacity="0.42" stroke-width="${strokeWidth}"/>`);
|
|
31846
|
-
if (y > 0 && y < height) labels.push(`<text x="4" y="${y - 4}">${y}</text>`);
|
|
31847
|
-
}
|
|
31848
|
-
return `<svg width="${width}" height="${height}" xmlns="http://www.w3.org/2000/svg">
|
|
31849
|
-
<style>text{font:16px monospace;fill:white;paint-order:stroke;stroke:black;stroke-width:4px;stroke-linejoin:round}</style>
|
|
31850
|
-
${lines.join("\n")}
|
|
31851
|
-
${labels.join("\n")}
|
|
31852
|
-
</svg>`;
|
|
31853
|
-
}
|
|
31854
|
-
function overlayGrid(rawPath, target, width, height, gridSize, gridPath) {
|
|
31855
|
-
writeFileSync5(gridPath, buildGridSvg(width, height, gridSize));
|
|
31856
|
-
const r = spawnSync3(
|
|
31857
|
-
"ffmpeg",
|
|
31858
|
-
[
|
|
31859
|
-
"-y",
|
|
31860
|
-
"-hide_banner",
|
|
31861
|
-
"-loglevel",
|
|
31862
|
-
"error",
|
|
31863
|
-
"-i",
|
|
31864
|
-
rawPath,
|
|
31865
|
-
"-i",
|
|
31866
|
-
gridPath,
|
|
31867
|
-
"-filter_complex",
|
|
31868
|
-
"[0:v][1:v]overlay=0:0:format=auto",
|
|
31869
|
-
"-frames:v",
|
|
31870
|
-
"1",
|
|
31871
|
-
"-update",
|
|
31872
|
-
"1",
|
|
31873
|
-
target
|
|
31874
|
-
],
|
|
31875
|
-
{ stdio: "pipe" }
|
|
31876
|
-
);
|
|
31877
|
-
if (r.status !== 0) {
|
|
31878
|
-
fail(`ffmpeg grid overlay failed: ${r.stderr?.toString().trim() || `exit ${r.status}`}`);
|
|
31879
|
-
}
|
|
31880
|
-
}
|
|
31881
|
-
async function computerScreenshotCommand(path6, options = {}) {
|
|
31882
|
-
const target = resolvePath(path6);
|
|
31883
|
-
mkdirSync5(dirname3(target), { recursive: true });
|
|
31884
|
-
const stamp = `${process.pid}-${Date.now()}`;
|
|
31885
|
-
const rawPath = `/tmp/replicas-screenshot-${stamp}.raw.png`;
|
|
31886
|
-
const svgPath = `/tmp/replicas-screenshot-${stamp}.brand.svg`;
|
|
31887
|
-
const maskPath = `/tmp/replicas-screenshot-${stamp}.mask.svg`;
|
|
31888
|
-
const shadowPath = `/tmp/replicas-screenshot-${stamp}.shadow.svg`;
|
|
31889
|
-
const gridPath = `/tmp/replicas-screenshot-${stamp}.grid.svg`;
|
|
31890
|
-
try {
|
|
31891
|
-
runDisplayCmd("scrot", ["-o", rawPath]);
|
|
31892
|
-
const { width, height } = readPngDimensions(rawPath);
|
|
31893
|
-
const gridSize = parseGridSize(options.grid);
|
|
31894
|
-
if (options.raw && gridSize === null) {
|
|
31895
|
-
copyFileSync2(rawPath, target);
|
|
31896
|
-
console.log(`${target} (${width}x${height}, raw 1:1 desktop pixels)`);
|
|
31897
|
-
return;
|
|
31898
|
-
}
|
|
31899
|
-
if (gridSize !== null) {
|
|
31900
|
-
overlayGrid(rawPath, target, width, height, gridSize, gridPath);
|
|
31901
|
-
console.log(`${target} (${width}x${height}, raw 1:1 desktop pixels, ${gridSize}px grid)`);
|
|
31902
|
-
return;
|
|
31903
|
-
}
|
|
31904
|
-
const padX = Math.round(width * BRAND_PAD_FRACTION);
|
|
31905
|
-
const padY = Math.round(height * BRAND_PAD_FRACTION);
|
|
31906
|
-
const canvasW = width + padX * 2;
|
|
31907
|
-
const canvasH = height + padY * 2;
|
|
31908
|
-
const minDim = Math.min(width, height);
|
|
31909
|
-
const cornerR = Math.round(minDim * SCREENSHOT_CORNER_FRACTION);
|
|
31910
|
-
const shadowSigma = Math.max(12, Math.round(minDim * SHADOW_SIGMA_FRACTION));
|
|
31911
|
-
const shadowOffsetY = Math.round(minDim * SHADOW_OFFSET_Y_FRACTION);
|
|
31912
|
-
const shadowMargin = shadowSigma * 3;
|
|
31913
|
-
const shadowW = width + shadowMargin * 2;
|
|
31914
|
-
const shadowH = height + shadowMargin * 2;
|
|
31915
|
-
writeFileSync5(svgPath, loadBrandSvg(canvasW, canvasH));
|
|
31916
|
-
writeFileSync5(
|
|
31917
|
-
maskPath,
|
|
31918
|
-
SCREENSHOT_MASK_TEMPLATE.replace(/__W__/g, String(width)).replace(/__H__/g, String(height)).replace(/__R__/g, String(cornerR))
|
|
31919
|
-
);
|
|
31920
|
-
writeFileSync5(
|
|
31921
|
-
shadowPath,
|
|
31922
|
-
SHADOW_MASK_TEMPLATE.replace(/__SW__/g, String(shadowW)).replace(/__SH__/g, String(shadowH)).replace(/__M__/g, String(shadowMargin)).replace(/__W__/g, String(width)).replace(/__H__/g, String(height)).replace(/__R__/g, String(cornerR))
|
|
31923
|
-
);
|
|
31924
|
-
const r = spawnSync3(
|
|
31925
|
-
"ffmpeg",
|
|
31926
|
-
[
|
|
31927
|
-
"-y",
|
|
31928
|
-
"-hide_banner",
|
|
31929
|
-
"-loglevel",
|
|
31930
|
-
"error",
|
|
31931
|
-
"-i",
|
|
31932
|
-
svgPath,
|
|
31933
|
-
"-i",
|
|
31934
|
-
rawPath,
|
|
31935
|
-
"-i",
|
|
31936
|
-
maskPath,
|
|
31937
|
-
"-i",
|
|
31938
|
-
shadowPath,
|
|
31939
|
-
"-filter_complex",
|
|
31940
|
-
`[2:v]format=rgba,alphaextract[mask];[3:v]format=rgba,colorchannelmixer=rr=0:gg=0:bb=0:aa=${SHADOW_ALPHA},gblur=sigma=${shadowSigma}[shadow];[1:v]format=rgba[scr];[scr][mask]alphamerge[rounded];[0:v]format=rgba[bg];[bg][shadow]overlay=${padX - shadowMargin}:${padY - shadowMargin + shadowOffsetY}:format=auto[bg_shadow];[bg_shadow][rounded]overlay=${padX}:${padY}:format=auto`,
|
|
31941
|
-
"-frames:v",
|
|
31942
|
-
"1",
|
|
31943
|
-
"-update",
|
|
31944
|
-
"1",
|
|
31945
|
-
target
|
|
31946
|
-
],
|
|
31947
|
-
{ stdio: "pipe" }
|
|
31948
|
-
);
|
|
31949
|
-
if (r.status !== 0) {
|
|
31950
|
-
fail(`ffmpeg branding failed: ${r.stderr?.toString().trim() || `exit ${r.status}`}`);
|
|
31951
|
-
}
|
|
31952
|
-
} finally {
|
|
31953
|
-
rmSync4(rawPath, { force: true });
|
|
31954
|
-
rmSync4(svgPath, { force: true });
|
|
31955
|
-
rmSync4(maskPath, { force: true });
|
|
31956
|
-
rmSync4(shadowPath, { force: true });
|
|
31957
|
-
rmSync4(gridPath, { force: true });
|
|
31958
|
-
}
|
|
31959
|
-
console.log(target);
|
|
31960
|
-
}
|
|
31961
|
-
function hashFile(path6) {
|
|
31962
|
-
return createHash3("sha256").update(readFileSync5(path6)).digest("hex");
|
|
31963
|
-
}
|
|
31964
|
-
async function captureStableRawScreenshot(target, options) {
|
|
31965
|
-
const start = Date.now();
|
|
31966
|
-
const framePath = `${target}.frame.png`;
|
|
31967
|
-
let lastHash = null;
|
|
31968
|
-
let lastChangeAt = start;
|
|
31969
|
-
let frames = 0;
|
|
31970
|
-
let changes = 0;
|
|
31971
|
-
let width = 0;
|
|
31972
|
-
let height = 0;
|
|
31973
|
-
try {
|
|
31974
|
-
while (Date.now() - start <= options.timeoutMs) {
|
|
31975
|
-
runDisplayCmd("scrot", ["-o", framePath]);
|
|
31976
|
-
frames++;
|
|
31977
|
-
const dimensions = readPngDimensions(framePath);
|
|
31978
|
-
width = dimensions.width;
|
|
31979
|
-
height = dimensions.height;
|
|
31980
|
-
const hash2 = hashFile(framePath);
|
|
31981
|
-
const now = Date.now();
|
|
31982
|
-
if (lastHash === null) {
|
|
31983
|
-
lastChangeAt = now;
|
|
31984
|
-
} else if (hash2 !== lastHash) {
|
|
31985
|
-
changes++;
|
|
31986
|
-
lastChangeAt = now;
|
|
31987
|
-
}
|
|
31988
|
-
lastHash = hash2;
|
|
31989
|
-
copyFileSync2(framePath, target);
|
|
31990
|
-
if (frames > 1 && now - lastChangeAt >= options.stableMs) {
|
|
31991
|
-
return { width, height, stable: true, elapsedMs: now - start, frames, changes };
|
|
31992
|
-
}
|
|
31993
|
-
await sleep2(options.pollMs);
|
|
31994
|
-
}
|
|
31995
|
-
return { width, height, stable: false, elapsedMs: Date.now() - start, frames, changes };
|
|
31996
|
-
} finally {
|
|
31997
|
-
rmSync4(framePath, { force: true });
|
|
31998
|
-
}
|
|
31999
|
-
}
|
|
32000
|
-
function getActiveWindowTitle() {
|
|
32001
|
-
return tryDisplayCmd("xdotool", ["getactivewindow", "getwindowname"]);
|
|
32002
|
-
}
|
|
32003
|
-
function getVisibleWindowTitles() {
|
|
32004
|
-
const out = tryDisplayCmd("xdotool", ["search", "--onlyvisible", "--name", ".", "getwindowname", "%@"]);
|
|
32005
|
-
return out ? out.split("\n").filter(Boolean).slice(0, 20) : [];
|
|
32006
|
-
}
|
|
32007
|
-
function recordingMousePosition() {
|
|
32008
|
-
const mouse = getMouseLocation();
|
|
32009
|
-
return mouse ? { x: mouse.x, y: mouse.y } : null;
|
|
32010
|
-
}
|
|
32011
|
-
async function computerObserveCommand(path6, options = {}) {
|
|
32012
|
-
const target = resolvePath(path6);
|
|
32013
|
-
mkdirSync5(dirname3(target), { recursive: true });
|
|
32014
|
-
const timeoutMs = options.timeout ? parseCoord(options.timeout, "--timeout") : 3e3;
|
|
32015
|
-
const stableMs = options.stableMs ? parseCoord(options.stableMs, "--stable-ms") : 600;
|
|
32016
|
-
const pollMs = options.pollMs ? parseCoord(options.pollMs, "--poll-ms") : 200;
|
|
32017
|
-
if (timeoutMs < 0) fail("--timeout must be >= 0");
|
|
32018
|
-
if (stableMs < 0) fail("--stable-ms must be >= 0");
|
|
32019
|
-
if (pollMs < 50 || pollMs > 2e3) fail("--poll-ms must be between 50 and 2000");
|
|
32020
|
-
const stamp = `${process.pid}-${Date.now()}`;
|
|
32021
|
-
const rawPath = `/tmp/replicas-observe-${stamp}.raw.png`;
|
|
32022
|
-
const gridPath = `/tmp/replicas-observe-${stamp}.grid.svg`;
|
|
32023
|
-
try {
|
|
32024
|
-
ensureServicesRunning();
|
|
32025
|
-
const capture = await captureStableRawScreenshot(rawPath, { timeoutMs, stableMs, pollMs });
|
|
32026
|
-
const gridSize = options.raw ? null : parseGridSize(options.grid ?? true);
|
|
32027
|
-
if (gridSize === null) {
|
|
32028
|
-
copyFileSync2(rawPath, target);
|
|
32029
|
-
} else {
|
|
32030
|
-
overlayGrid(rawPath, target, capture.width, capture.height, gridSize, gridPath);
|
|
32031
|
-
}
|
|
32032
|
-
console.log(JSON.stringify({
|
|
32033
|
-
screenshot: target,
|
|
32034
|
-
width: capture.width,
|
|
32035
|
-
height: capture.height,
|
|
32036
|
-
stable: capture.stable,
|
|
32037
|
-
elapsedMs: capture.elapsedMs,
|
|
32038
|
-
frames: capture.frames,
|
|
32039
|
-
changes: capture.changes,
|
|
32040
|
-
mouse: getMouseLocation(),
|
|
32041
|
-
activeWindow: getActiveWindowTitle(),
|
|
32042
|
-
visibleWindows: getVisibleWindowTitles(),
|
|
32043
|
-
mode: gridSize === null ? "raw" : "grid",
|
|
32044
|
-
gridSize
|
|
32045
|
-
}, null, 2));
|
|
32046
|
-
} finally {
|
|
32047
|
-
rmSync4(rawPath, { force: true });
|
|
32048
|
-
rmSync4(gridPath, { force: true });
|
|
32049
|
-
}
|
|
32050
|
-
}
|
|
32051
|
-
async function fetchChromeJson(path6) {
|
|
32052
|
-
let res;
|
|
32053
|
-
try {
|
|
32054
|
-
res = await fetch(`http://127.0.0.1:${CHROME_DEBUG_PORT}${path6}`);
|
|
32055
|
-
} catch {
|
|
32056
|
-
fail(
|
|
32057
|
-
`Chrome DevTools is not available on localhost:${CHROME_DEBUG_PORT}. Launch Chrome with \`replicas computer launch chrome <url>\` and try again.`
|
|
32058
|
-
);
|
|
32059
|
-
}
|
|
32060
|
-
if (!res.ok) fail(`Chrome DevTools returned HTTP ${res.status}`);
|
|
32061
|
-
try {
|
|
32062
|
-
return await res.json();
|
|
32063
|
-
} catch (error51) {
|
|
32064
|
-
const reason = error51 instanceof Error ? error51.message : String(error51);
|
|
32065
|
-
fail(`Chrome DevTools returned invalid JSON from ${path6}: ${reason}`);
|
|
32066
|
-
}
|
|
32067
|
-
}
|
|
32068
|
-
async function getChromePages() {
|
|
32069
|
-
const targets = await fetchChromeJson("/json/list");
|
|
32070
|
-
if (!Array.isArray(targets)) fail("Chrome DevTools returned an unexpected target list");
|
|
32071
|
-
return targets.filter((target) => typeof target === "object" && target !== null).filter((target) => target.type === "page");
|
|
32072
|
-
}
|
|
32073
|
-
async function selectChromePage(options = {}) {
|
|
32074
|
-
const pages = await getChromePages();
|
|
32075
|
-
let matches = pages;
|
|
32076
|
-
if (options.targetId) {
|
|
32077
|
-
matches = matches.filter((page2) => page2.id === options.targetId);
|
|
32078
|
-
}
|
|
32079
|
-
if (options.title) {
|
|
32080
|
-
const needle = options.title.toLowerCase();
|
|
32081
|
-
matches = matches.filter((page2) => (page2.title ?? "").toLowerCase().includes(needle));
|
|
32082
|
-
}
|
|
32083
|
-
if (options.url) {
|
|
32084
|
-
const needle = options.url.toLowerCase();
|
|
32085
|
-
matches = matches.filter((page2) => (page2.url ?? "").toLowerCase().includes(needle));
|
|
32086
|
-
}
|
|
32087
|
-
const index = options.page ? parseCoord(options.page, "--page") : 0;
|
|
32088
|
-
if (index < 0) fail("--page must be >= 0");
|
|
32089
|
-
const page = matches[index];
|
|
32090
|
-
const webSocketDebuggerUrl = page?.webSocketDebuggerUrl;
|
|
32091
|
-
if (!webSocketDebuggerUrl) {
|
|
32092
|
-
fail(
|
|
32093
|
-
`No matching debuggable Chrome page found (${matches.length} match${matches.length === 1 ? "" : "es"}, index ${index}). Use \`replicas computer browser\` to list pages.`
|
|
32094
|
-
);
|
|
32095
|
-
}
|
|
32096
|
-
await sendChromeCommand(webSocketDebuggerUrl, "Page.bringToFront", {});
|
|
32097
|
-
return { ...page, webSocketDebuggerUrl };
|
|
32098
|
-
}
|
|
32099
|
-
var VISIBLE_JS = `
|
|
32100
|
-
const visible = (el) => {
|
|
32101
|
-
const rect = el.getBoundingClientRect();
|
|
32102
|
-
const style = getComputedStyle(el);
|
|
32103
|
-
return rect.width > 0 && rect.height > 0 && style.visibility !== 'hidden' && style.display !== 'none';
|
|
32104
|
-
};`;
|
|
32105
|
-
async function sendChromeSessionCommand(session, method, params) {
|
|
32106
|
-
return chromeResult(await session.send({ method, params }), method);
|
|
32107
|
-
}
|
|
32108
|
-
async function withChromeSession(webSocketDebuggerUrl, callback) {
|
|
32109
|
-
return await new Promise((resolve3, reject) => {
|
|
32110
|
-
const ws = new WebSocket(webSocketDebuggerUrl);
|
|
32111
|
-
const pending = /* @__PURE__ */ new Map();
|
|
32112
|
-
let nextId = 1;
|
|
32113
|
-
let settled = false;
|
|
32114
|
-
const failSession = (error51) => {
|
|
32115
|
-
if (settled) return;
|
|
32116
|
-
settled = true;
|
|
32117
|
-
for (const request of pending.values()) {
|
|
32118
|
-
clearTimeout(request.timeout);
|
|
32119
|
-
request.reject(error51);
|
|
32120
|
-
}
|
|
32121
|
-
pending.clear();
|
|
32122
|
-
ws.close();
|
|
32123
|
-
reject(error51);
|
|
32124
|
-
};
|
|
32125
|
-
const finishSession = (value) => {
|
|
32126
|
-
if (settled) return;
|
|
32127
|
-
settled = true;
|
|
32128
|
-
for (const request of pending.values()) clearTimeout(request.timeout);
|
|
32129
|
-
pending.clear();
|
|
32130
|
-
ws.close();
|
|
32131
|
-
resolve3(value);
|
|
32132
|
-
};
|
|
32133
|
-
const session = {
|
|
32134
|
-
send: ({ method, params }) => new Promise((resolveCommand, rejectCommand) => {
|
|
32135
|
-
const id = nextId++;
|
|
32136
|
-
const timeout = setTimeout(() => {
|
|
32137
|
-
pending.delete(id);
|
|
32138
|
-
rejectCommand(new Error(`Chrome DevTools ${method} timed out`));
|
|
32139
|
-
}, 1e4);
|
|
32140
|
-
pending.set(id, { method, resolve: resolveCommand, reject: rejectCommand, timeout });
|
|
32141
|
-
ws.send(JSON.stringify({ id, method, params }));
|
|
32142
|
-
})
|
|
32143
|
-
};
|
|
32144
|
-
ws.addEventListener("open", async () => {
|
|
32145
|
-
try {
|
|
32146
|
-
finishSession(await callback(session));
|
|
32147
|
-
} catch (error51) {
|
|
32148
|
-
failSession(error51 instanceof Error ? error51 : new Error(String(error51)));
|
|
32149
|
-
}
|
|
32150
|
-
});
|
|
32151
|
-
ws.addEventListener("message", (event) => {
|
|
32152
|
-
const data = typeof event.data === "string" ? event.data : Buffer.isBuffer(event.data) ? event.data.toString("utf8") : Buffer.from(event.data).toString("utf8");
|
|
32153
|
-
let message;
|
|
32154
|
-
try {
|
|
32155
|
-
message = JSON.parse(data);
|
|
32156
|
-
} catch (error51) {
|
|
32157
|
-
failSession(new Error(`Chrome DevTools returned invalid WebSocket JSON: ${error51 instanceof Error ? error51.message : String(error51)}`));
|
|
32158
|
-
return;
|
|
32159
|
-
}
|
|
32160
|
-
if (!isRecord(message) || typeof message.id !== "number") return;
|
|
32161
|
-
const request = pending.get(message.id);
|
|
32162
|
-
if (!request) return;
|
|
32163
|
-
clearTimeout(request.timeout);
|
|
32164
|
-
pending.delete(message.id);
|
|
32165
|
-
const detail = isRecord(message.error) && typeof message.error.message === "string" ? message.error.message : void 0;
|
|
32166
|
-
request.resolve(message.error ? { error: { message: detail } } : { result: isRecord(message.result) ? message.result : {} });
|
|
32167
|
-
});
|
|
32168
|
-
ws.addEventListener("error", () => failSession(new Error("Chrome DevTools WebSocket failed")));
|
|
32169
|
-
ws.addEventListener("close", () => {
|
|
32170
|
-
if (!settled) failSession(new Error("Chrome DevTools WebSocket closed unexpectedly"));
|
|
32171
|
-
});
|
|
32172
|
-
});
|
|
32173
|
-
}
|
|
32174
|
-
async function sendChromeCommands(webSocketDebuggerUrl, commands) {
|
|
32175
|
-
return await withChromeSession(webSocketDebuggerUrl, async (session) => await Promise.all(commands.map((command) => session.send(command))));
|
|
32176
|
-
}
|
|
32177
|
-
async function sendChromeCommand(webSocketDebuggerUrl, method, params) {
|
|
32178
|
-
const response = (await sendChromeCommands(webSocketDebuggerUrl, [{ method, params }]))[0];
|
|
32179
|
-
if (response.error) throw new Error(response.error.message || `Chrome DevTools ${method} failed`);
|
|
32180
|
-
return response.result ?? {};
|
|
32181
|
-
}
|
|
32182
|
-
async function callFunctionOnChromeNodeInSession(session, backendNodeId, functionDeclaration, args = []) {
|
|
32183
|
-
const resolved = await sendChromeSessionCommand(session, "DOM.resolveNode", { backendNodeId });
|
|
32184
|
-
const objectId = isRecord(resolved.object) ? resolved.object.objectId : void 0;
|
|
32185
|
-
if (typeof objectId !== "string") throw new Error(`Could not resolve browser element ref ${backendNodeId}`);
|
|
32186
|
-
return await sendChromeSessionCommand(session, "Runtime.callFunctionOn", {
|
|
32187
|
-
objectId,
|
|
32188
|
-
functionDeclaration,
|
|
32189
|
-
arguments: args.map((value) => ({ value })),
|
|
32190
|
-
returnByValue: true
|
|
32191
|
-
});
|
|
32192
|
-
}
|
|
32193
|
-
function chromeCallValue(response) {
|
|
32194
|
-
return isRecord(response.result) ? response.result.value : void 0;
|
|
32195
|
-
}
|
|
32196
|
-
async function evaluateChromeTarget(webSocketDebuggerUrl, expression) {
|
|
32197
|
-
return await withChromeSession(webSocketDebuggerUrl, async (session) => await evaluateChromeSession(session, expression));
|
|
32198
|
-
}
|
|
32199
|
-
async function evaluateChromeSession(session, expression) {
|
|
32200
|
-
const response = await sendChromeSessionCommand(session, "Runtime.evaluate", {
|
|
32201
|
-
expression,
|
|
32202
|
-
awaitPromise: true,
|
|
32203
|
-
returnByValue: true
|
|
32204
|
-
});
|
|
32205
|
-
const exception = response.exceptionDetails;
|
|
32206
|
-
if (exception) {
|
|
32207
|
-
const text = typeof exception === "object" && exception !== null && "text" in exception && typeof exception.text === "string" ? exception.text : "";
|
|
32208
|
-
throw new Error(text || "Chrome DevTools evaluation threw");
|
|
32209
|
-
}
|
|
32210
|
-
const evaluated = response.result;
|
|
32211
|
-
return typeof evaluated === "object" && evaluated !== null && "value" in evaluated ? evaluated.value ?? null : null;
|
|
32212
|
-
}
|
|
32213
|
-
function isRawAXNode(value) {
|
|
32214
|
-
return isRecord(value);
|
|
32215
|
-
}
|
|
32216
|
-
function isDOMSnapshotDocument(value) {
|
|
32217
|
-
return isRecord(value);
|
|
32218
|
-
}
|
|
32219
|
-
function chromeVisualViewport(layout) {
|
|
32220
|
-
const viewport = isRecord(layout.cssVisualViewport) ? layout.cssVisualViewport : {};
|
|
32221
|
-
return {
|
|
32222
|
-
width: typeof viewport.clientWidth === "number" ? viewport.clientWidth : 0,
|
|
32223
|
-
height: typeof viewport.clientHeight === "number" ? viewport.clientHeight : 0
|
|
32224
|
-
};
|
|
32225
|
-
}
|
|
32226
|
-
function roundBrowserRect(rect) {
|
|
32227
|
-
return {
|
|
32228
|
-
x: Math.round(rect.x),
|
|
32229
|
-
y: Math.round(rect.y),
|
|
32230
|
-
width: Math.round(rect.width),
|
|
32231
|
-
height: Math.round(rect.height),
|
|
32232
|
-
centerX: Math.round(rect.centerX),
|
|
32233
|
-
centerY: Math.round(rect.centerY)
|
|
32234
|
-
};
|
|
32235
|
-
}
|
|
32236
|
-
function browserStateProperties(node) {
|
|
32237
|
-
const properties = [];
|
|
32238
|
-
for (const { name, value } of node.properties ?? []) {
|
|
32239
|
-
const state = value?.value;
|
|
32240
|
-
if (!name || !BROWSER_STATE_PROPERTIES.has(name) || typeof state !== "string" && typeof state !== "number" && typeof state !== "boolean") continue;
|
|
32241
|
-
properties.push([name, name === "url" ? clippedText(state, 300) : state]);
|
|
32242
|
-
}
|
|
32243
|
-
return Object.fromEntries(properties);
|
|
32244
|
-
}
|
|
32245
|
-
function readBrowserStateCache(path6) {
|
|
32246
|
-
let value;
|
|
32247
|
-
try {
|
|
32248
|
-
value = JSON.parse(readFileSync5(path6, "utf8"));
|
|
32249
|
-
} catch {
|
|
32250
|
-
return null;
|
|
32251
|
-
}
|
|
32252
|
-
if (!isRecord(value) || !Array.isArray(value.entries)) return null;
|
|
32253
|
-
const entries = [];
|
|
32254
|
-
for (const entry of value.entries) {
|
|
32255
|
-
if (!isRecord(entry) || typeof entry.key !== "string" || typeof entry.semantic !== "string" || typeof entry.line !== "string") return null;
|
|
32256
|
-
entries.push({ key: entry.key, semantic: entry.semantic, line: entry.line });
|
|
32257
|
-
}
|
|
32258
|
-
return {
|
|
32259
|
-
url: typeof value.url === "string" ? value.url : void 0,
|
|
32260
|
-
documentId: typeof value.documentId === "string" || value.documentId === null ? value.documentId : void 0,
|
|
32261
|
-
entries
|
|
32262
|
-
};
|
|
32263
|
-
}
|
|
32264
|
-
function chromeResult(response, method) {
|
|
32265
|
-
if (response.error) throw new Error(response.error.message || `Chrome DevTools ${method} failed`);
|
|
32266
|
-
return response.result ?? {};
|
|
32267
|
-
}
|
|
32268
|
-
function collectFrameIds(frameTree) {
|
|
32269
|
-
const ids = [];
|
|
32270
|
-
const visit = (value) => {
|
|
32271
|
-
if (!isRecord(value)) return;
|
|
32272
|
-
const frame = isRecord(value.frame) ? value.frame : {};
|
|
32273
|
-
if (typeof frame.id === "string") ids.push(frame.id);
|
|
32274
|
-
if (Array.isArray(value.childFrames)) value.childFrames.forEach(visit);
|
|
32275
|
-
};
|
|
32276
|
-
visit(frameTree);
|
|
32277
|
-
return ids;
|
|
32278
|
-
}
|
|
32279
|
-
function browserDocumentId(frameTree) {
|
|
32280
|
-
if (!isRecord(frameTree) || !isRecord(frameTree.frame)) return null;
|
|
32281
|
-
const loaderId = frameTree.frame.loaderId;
|
|
32282
|
-
return typeof loaderId === "string" ? loaderId : null;
|
|
32283
|
-
}
|
|
32284
|
-
function intersectRects(a, b) {
|
|
32285
|
-
const x = Math.max(a.x, b.x);
|
|
32286
|
-
const y = Math.max(a.y, b.y);
|
|
32287
|
-
const right = Math.min(a.x + a.width, b.x + b.width);
|
|
32288
|
-
const bottom = Math.min(a.y + a.height, b.y + b.height);
|
|
32289
|
-
if (right <= x || bottom <= y) return null;
|
|
32290
|
-
return {
|
|
32291
|
-
x,
|
|
32292
|
-
y,
|
|
32293
|
-
width: right - x,
|
|
32294
|
-
height: bottom - y,
|
|
32295
|
-
centerX: x + (right - x) / 2,
|
|
32296
|
-
centerY: y + (bottom - y) / 2
|
|
32297
|
-
};
|
|
32298
|
-
}
|
|
32299
|
-
function browserContentQuadRects(quads, viewportRect) {
|
|
32300
|
-
if (!Array.isArray(quads)) return [];
|
|
32301
|
-
return quads.filter((quad) => Array.isArray(quad) && quad.length >= 8 && quad.every(Number.isFinite)).map((quad) => {
|
|
32302
|
-
const xs = [quad[0], quad[2], quad[4], quad[6]];
|
|
32303
|
-
const ys = [quad[1], quad[3], quad[5], quad[7]];
|
|
32304
|
-
const x = Math.min(...xs);
|
|
32305
|
-
const y = Math.min(...ys);
|
|
32306
|
-
const width = Math.max(...xs) - x;
|
|
32307
|
-
const height = Math.max(...ys) - y;
|
|
32308
|
-
return { x, y, width, height, centerX: x + width / 2, centerY: y + height / 2 };
|
|
32309
|
-
}).map((rect) => intersectRects(rect, viewportRect)).filter((rect) => !!rect).sort((a, b) => b.width * b.height - a.width * a.height);
|
|
32310
|
-
}
|
|
32311
|
-
function buildBrowserBounds(result, viewport, targetId) {
|
|
32312
|
-
const strings = Array.isArray(result.strings) ? result.strings : [];
|
|
32313
|
-
const documents = Array.isArray(result.documents) ? result.documents.filter(isDOMSnapshotDocument) : [];
|
|
32314
|
-
const localRects = documents.map((document) => {
|
|
32315
|
-
const rects = /* @__PURE__ */ new Map();
|
|
32316
|
-
const nodeIndexes = document.layout?.nodeIndex ?? [];
|
|
32317
|
-
const bounds = document.layout?.bounds ?? [];
|
|
32318
|
-
const scrollX = document.scrollOffsetX ?? 0;
|
|
32319
|
-
const scrollY = document.scrollOffsetY ?? 0;
|
|
32320
|
-
nodeIndexes.forEach((nodeIndex, index) => {
|
|
32321
|
-
const bound = bounds[index];
|
|
32322
|
-
if (!Array.isArray(bound) || bound.length < 4 || !bound.every(Number.isFinite)) return;
|
|
32323
|
-
const [rawX, rawY, width, height] = bound;
|
|
32324
|
-
rects.set(nodeIndex, {
|
|
32325
|
-
x: rawX - scrollX,
|
|
32326
|
-
y: rawY - scrollY,
|
|
32327
|
-
width,
|
|
32328
|
-
height,
|
|
32329
|
-
centerX: rawX - scrollX + width / 2,
|
|
32330
|
-
centerY: rawY - scrollY + height / 2
|
|
32331
|
-
});
|
|
32332
|
-
});
|
|
32333
|
-
return rects;
|
|
32334
|
-
});
|
|
32335
|
-
const parents = /* @__PURE__ */ new Map();
|
|
32336
|
-
documents.forEach((document, documentIndex) => {
|
|
32337
|
-
const sparse = document.nodes?.contentDocumentIndex;
|
|
32338
|
-
sparse?.index?.forEach((nodeIndex, index) => {
|
|
32339
|
-
const childDocumentIndex = sparse.value?.[index];
|
|
32340
|
-
if (typeof childDocumentIndex === "number") parents.set(childDocumentIndex, { documentIndex, nodeIndex });
|
|
32341
|
-
});
|
|
32342
|
-
});
|
|
32343
|
-
const targetDocumentIndex = documents.findIndex(
|
|
32344
|
-
(document) => typeof document.frameId === "number" && strings[document.frameId] === targetId
|
|
32345
|
-
);
|
|
32346
|
-
const rootIndex = targetDocumentIndex >= 0 ? targetDocumentIndex : documents.findIndex((_, index) => !parents.has(index));
|
|
32347
|
-
const viewportRect = {
|
|
32348
|
-
x: 0,
|
|
32349
|
-
y: 0,
|
|
32350
|
-
width: viewport.width,
|
|
32351
|
-
height: viewport.height,
|
|
32352
|
-
centerX: viewport.width / 2,
|
|
32353
|
-
centerY: viewport.height / 2
|
|
32354
|
-
};
|
|
32355
|
-
const origins = /* @__PURE__ */ new Map();
|
|
32356
|
-
origins.set(rootIndex < 0 ? 0 : rootIndex, { x: 0, y: 0, clip: viewportRect });
|
|
32357
|
-
for (let pass = 0; pass < documents.length; pass++) {
|
|
32358
|
-
let changed = false;
|
|
32359
|
-
for (const [childIndex, parent] of parents) {
|
|
32360
|
-
if (origins.has(childIndex)) continue;
|
|
32361
|
-
const parentOrigin = origins.get(parent.documentIndex);
|
|
32362
|
-
const frameRect = localRects[parent.documentIndex]?.get(parent.nodeIndex);
|
|
32363
|
-
if (!parentOrigin || !frameRect) continue;
|
|
32364
|
-
const globalFrameRect = {
|
|
32365
|
-
...frameRect,
|
|
32366
|
-
x: parentOrigin.x + frameRect.x,
|
|
32367
|
-
y: parentOrigin.y + frameRect.y,
|
|
32368
|
-
centerX: parentOrigin.x + frameRect.centerX,
|
|
32369
|
-
centerY: parentOrigin.y + frameRect.centerY
|
|
32370
|
-
};
|
|
32371
|
-
const childDocument = documents[childIndex];
|
|
32372
|
-
const insetX = Math.max(0, (globalFrameRect.width - (childDocument?.contentWidth ?? globalFrameRect.width)) / 2);
|
|
32373
|
-
const insetY = Math.max(0, (globalFrameRect.height - (childDocument?.contentHeight ?? globalFrameRect.height)) / 2);
|
|
32374
|
-
const contentRect = {
|
|
32375
|
-
x: globalFrameRect.x + insetX,
|
|
32376
|
-
y: globalFrameRect.y + insetY,
|
|
32377
|
-
width: Math.max(0, globalFrameRect.width - insetX * 2),
|
|
32378
|
-
height: Math.max(0, globalFrameRect.height - insetY * 2),
|
|
32379
|
-
centerX: globalFrameRect.centerX,
|
|
32380
|
-
centerY: globalFrameRect.centerY
|
|
32381
|
-
};
|
|
32382
|
-
const clip = intersectRects(parentOrigin.clip, contentRect);
|
|
32383
|
-
if (!clip) continue;
|
|
32384
|
-
origins.set(childIndex, { x: contentRect.x, y: contentRect.y, clip });
|
|
32385
|
-
changed = true;
|
|
32386
|
-
}
|
|
32387
|
-
if (!changed) break;
|
|
32388
|
-
}
|
|
32389
|
-
const byBackendNodeId = /* @__PURE__ */ new Map();
|
|
32390
|
-
documents.forEach((document, documentIndex) => {
|
|
32391
|
-
const origin = origins.get(documentIndex);
|
|
32392
|
-
if (!origin) return;
|
|
32393
|
-
const backendNodeIds = document.nodes?.backendNodeId ?? [];
|
|
32394
|
-
for (const [nodeIndex, local] of localRects[documentIndex] ?? []) {
|
|
32395
|
-
const backendNodeId = backendNodeIds[nodeIndex];
|
|
32396
|
-
if (!Number.isInteger(backendNodeId)) continue;
|
|
32397
|
-
const rect = {
|
|
32398
|
-
...local,
|
|
32399
|
-
x: origin.x + local.x,
|
|
32400
|
-
y: origin.y + local.y,
|
|
32401
|
-
centerX: origin.x + local.centerX,
|
|
32402
|
-
centerY: origin.y + local.centerY
|
|
32403
|
-
};
|
|
32404
|
-
if (intersectRects(rect, origin.clip)) byBackendNodeId.set(backendNodeId, { rect, clip: origin.clip });
|
|
32405
|
-
}
|
|
32406
|
-
});
|
|
32407
|
-
return byBackendNodeId;
|
|
32408
|
-
}
|
|
32409
|
-
var BROWSER_INTERACTIVE_ROLES = /* @__PURE__ */ new Set([
|
|
32410
|
-
"button",
|
|
32411
|
-
"checkbox",
|
|
32412
|
-
"combobox",
|
|
32413
|
-
"link",
|
|
32414
|
-
"listbox",
|
|
32415
|
-
"menuitem",
|
|
32416
|
-
"menuitemcheckbox",
|
|
32417
|
-
"menuitemradio",
|
|
32418
|
-
"option",
|
|
32419
|
-
"radio",
|
|
32420
|
-
"scrollbar",
|
|
32421
|
-
"searchbox",
|
|
32422
|
-
"slider",
|
|
32423
|
-
"spinbutton",
|
|
32424
|
-
"switch",
|
|
32425
|
-
"tab",
|
|
32426
|
-
"textbox",
|
|
32427
|
-
"treeitem"
|
|
32428
|
-
]);
|
|
32429
|
-
var BROWSER_STRUCTURAL_ROLES = /* @__PURE__ */ new Set([
|
|
32430
|
-
"alert",
|
|
32431
|
-
"cell",
|
|
32432
|
-
"columnheader",
|
|
32433
|
-
"dialog",
|
|
32434
|
-
"figure",
|
|
32435
|
-
"gridcell",
|
|
32436
|
-
"heading",
|
|
32437
|
-
"image",
|
|
32438
|
-
"listitem",
|
|
32439
|
-
"main",
|
|
32440
|
-
"paragraph",
|
|
32441
|
-
"region",
|
|
32442
|
-
"row",
|
|
32443
|
-
"rowheader",
|
|
32444
|
-
"status",
|
|
32445
|
-
"StaticText"
|
|
32446
|
-
]);
|
|
32447
|
-
var BROWSER_STATE_PROPERTIES = /* @__PURE__ */ new Set([
|
|
32448
|
-
"autocomplete",
|
|
32449
|
-
"checked",
|
|
32450
|
-
"disabled",
|
|
32451
|
-
"editable",
|
|
32452
|
-
"expanded",
|
|
32453
|
-
"focusable",
|
|
32454
|
-
"focused",
|
|
32455
|
-
"haspopup",
|
|
32456
|
-
"invalid",
|
|
32457
|
-
"level",
|
|
32458
|
-
"multiselectable",
|
|
32459
|
-
"orientation",
|
|
32460
|
-
"pressed",
|
|
32461
|
-
"readonly",
|
|
32462
|
-
"protected",
|
|
32463
|
-
"required",
|
|
32464
|
-
"selected",
|
|
32465
|
-
"url"
|
|
32466
|
-
]);
|
|
32467
|
-
function clippedText(value, limit = 240) {
|
|
32468
|
-
return String(value ?? "").replace(/\s+/g, " ").trim().slice(0, limit);
|
|
32469
|
-
}
|
|
32470
|
-
function browserTargetOutput(page) {
|
|
32471
|
-
return {
|
|
32472
|
-
targetId: page.id ?? null,
|
|
32473
|
-
title: (page.title ?? "").slice(0, 1e3),
|
|
32474
|
-
url: (page.url ?? "").slice(0, 2e3)
|
|
32475
|
-
};
|
|
32476
|
-
}
|
|
32477
|
-
function browserNodeActionable(role, states) {
|
|
32478
|
-
return BROWSER_INTERACTIVE_ROLES.has(role) || states.editable === true || states.focusable === true && role !== "RootWebArea";
|
|
32479
|
-
}
|
|
32480
|
-
async function captureBrowserSnapshot(page, options) {
|
|
32481
|
-
const initialCommands = [
|
|
32482
|
-
{ method: "Page.getFrameTree", params: {} },
|
|
32483
|
-
{ method: "Page.getLayoutMetrics", params: {} },
|
|
32484
|
-
{ method: "DOMSnapshot.captureSnapshot", params: { computedStyles: [], includeDOMRects: true, includePaintOrder: true } }
|
|
32485
|
-
];
|
|
32486
|
-
const initial = await sendChromeCommands(page.webSocketDebuggerUrl, initialCommands);
|
|
32487
|
-
const frameTree = chromeResult(initial[0], initialCommands[0].method).frameTree;
|
|
32488
|
-
const layout = chromeResult(initial[1], initialCommands[1].method);
|
|
32489
|
-
const domSnapshot = chromeResult(initial[2], initialCommands[2].method);
|
|
32490
|
-
const viewport = chromeVisualViewport(layout);
|
|
32491
|
-
const frameIds = collectFrameIds(frameTree);
|
|
32492
|
-
if (frameIds.length === 0 && page.id) frameIds.push(page.id);
|
|
32493
|
-
const axCommands = frameIds.map((frameId) => ({
|
|
32494
|
-
method: "Accessibility.getFullAXTree",
|
|
32495
|
-
params: { frameId }
|
|
32496
|
-
}));
|
|
32497
|
-
const axResults = await sendChromeCommands(page.webSocketDebuggerUrl, axCommands);
|
|
32498
|
-
const nodes = [];
|
|
32499
|
-
axResults.forEach((response, index) => {
|
|
32500
|
-
if (response.error) return;
|
|
32501
|
-
const responseNodes = response.result?.nodes;
|
|
32502
|
-
if (!Array.isArray(responseNodes)) return;
|
|
32503
|
-
for (const node of responseNodes) {
|
|
32504
|
-
if (!isRawAXNode(node)) continue;
|
|
32505
|
-
nodes.push({ ...node, sourceFrameId: frameIds[index] });
|
|
32506
|
-
}
|
|
32507
|
-
});
|
|
32508
|
-
const bounds = buildBrowserBounds(domSnapshot, viewport, page.id);
|
|
32509
|
-
const nodesByKey = new Map(nodes.map((node) => [`${node.sourceFrameId}:${node.nodeId}`, node]));
|
|
32510
|
-
const propertyMap = browserStateProperties;
|
|
32511
|
-
const roleOf = (node) => clippedText(node.role?.value, 80);
|
|
32512
|
-
const nameOf = (node) => clippedText(node.name?.value);
|
|
32513
|
-
const valueOf = (node) => {
|
|
32514
|
-
if (propertyMap(node).protected === true) return null;
|
|
32515
|
-
const value = node.value?.value;
|
|
32516
|
-
if (typeof value === "number" || typeof value === "boolean") return value;
|
|
32517
|
-
const text = clippedText(value);
|
|
32518
|
-
if (/^[•●*]+$/.test(text)) return "[protected]";
|
|
32519
|
-
return text || null;
|
|
32520
|
-
};
|
|
32521
|
-
const meaningful = nodes.filter((node) => !node.ignored && Number.isInteger(node.backendDOMNodeId)).map((node) => ({
|
|
32522
|
-
node,
|
|
32523
|
-
role: roleOf(node),
|
|
32524
|
-
bound: typeof node.backendDOMNodeId === "number" ? bounds.get(node.backendDOMNodeId) : void 0
|
|
32525
|
-
})).filter(({ node, role, bound }) => {
|
|
32526
|
-
if (!bound || bound.rect.width <= 0 || bound.rect.height <= 0) return false;
|
|
32527
|
-
if (role === "InlineTextBox" || role === "none" || role === "generic") return false;
|
|
32528
|
-
if (BROWSER_INTERACTIVE_ROLES.has(role) || BROWSER_STRUCTURAL_ROLES.has(role)) return true;
|
|
32529
|
-
return !!nameOf(node) || valueOf(node) !== null || propertyMap(node).focusable === true;
|
|
32530
|
-
}).filter(({ node, role }) => {
|
|
32531
|
-
if (role !== "StaticText") return true;
|
|
32532
|
-
const parent = node.parentId ? nodesByKey.get(`${node.sourceFrameId}:${node.parentId}`) : void 0;
|
|
32533
|
-
return !parent || nameOf(parent) !== nameOf(node);
|
|
32534
|
-
}).sort((a, b) => a.bound.rect.y - b.bound.rect.y || a.bound.rect.x - b.bound.rect.x);
|
|
32535
|
-
const elements = [];
|
|
32536
|
-
const entries = [];
|
|
32537
|
-
const meaningfulKeys = new Set(meaningful.map(({ node }) => `${node.sourceFrameId}:${node.nodeId}`));
|
|
32538
|
-
const visibleText = [];
|
|
32539
|
-
let visibleTextLength = 0;
|
|
32540
|
-
let treeLength = 0;
|
|
32541
|
-
const treeLimit = Math.min(5e4, Math.max(4e3, options.textLimit * 2));
|
|
32542
|
-
for (const { node, role, bound } of meaningful) {
|
|
32543
|
-
const name = nameOf(node);
|
|
32544
|
-
const description = clippedText(node.description?.value) || null;
|
|
32545
|
-
const value = valueOf(node);
|
|
32546
|
-
const states = propertyMap(node);
|
|
32547
|
-
const actionable = browserNodeActionable(role, states);
|
|
32548
|
-
const ref = actionable && elements.length < options.elementLimit ? String(node.backendDOMNodeId) : null;
|
|
32549
|
-
const visibleRect = role === "RootWebArea" && node.sourceFrameId === page.id ? { x: 0, y: 0, width: viewport.width, height: viewport.height, centerX: viewport.width / 2, centerY: viewport.height / 2 } : intersectRects(bound.rect, bound.clip) ?? bound.rect;
|
|
32550
|
-
const rect = roundBrowserRect(visibleRect);
|
|
32551
|
-
const actions = [
|
|
32552
|
-
...actionable ? ["click"] : [],
|
|
32553
|
-
...["textbox", "searchbox", "combobox", "spinbutton", "slider"].includes(role) || states.editable === true ? ["set_value"] : [],
|
|
32554
|
-
"scroll"
|
|
32555
|
-
];
|
|
32556
|
-
if (ref) {
|
|
32557
|
-
elements.push({
|
|
32558
|
-
ref,
|
|
32559
|
-
role,
|
|
32560
|
-
name: name || null,
|
|
32561
|
-
description,
|
|
32562
|
-
value,
|
|
32563
|
-
states,
|
|
32564
|
-
actions,
|
|
32565
|
-
rect,
|
|
32566
|
-
frameId: node.sourceFrameId
|
|
32567
|
-
});
|
|
32568
|
-
}
|
|
32569
|
-
const stateText = Object.entries(states).filter(([key]) => key !== "url" && key !== "focusable").map(([key, state]) => `${key}=${JSON.stringify(state)}`);
|
|
32570
|
-
const url2 = typeof states.url === "string" ? clippedText(states.url, 300) : "";
|
|
32571
|
-
const semantic = [role, name && JSON.stringify(name), value !== null && `value=${JSON.stringify(value)}`, description && `description=${JSON.stringify(description)}`, url2 && `url=${JSON.stringify(url2)}`, ...stateText].filter(Boolean).join(" ");
|
|
32572
|
-
let depth = 0;
|
|
32573
|
-
let parentId = node.parentId;
|
|
32574
|
-
const visited = /* @__PURE__ */ new Set();
|
|
32575
|
-
while (parentId && depth < 8) {
|
|
32576
|
-
const parentKey = `${node.sourceFrameId}:${parentId}`;
|
|
32577
|
-
if (visited.has(parentKey)) break;
|
|
32578
|
-
visited.add(parentKey);
|
|
32579
|
-
if (meaningfulKeys.has(parentKey)) depth++;
|
|
32580
|
-
parentId = nodesByKey.get(parentKey)?.parentId;
|
|
32581
|
-
}
|
|
32582
|
-
const line = `${" ".repeat(depth)}${ref ? `[ref=${ref}]` : "-"} ${semantic} (${rect.x},${rect.y} ${rect.width}x${rect.height})`;
|
|
32583
|
-
if (treeLength + line.length + 1 <= treeLimit) {
|
|
32584
|
-
const key = Number.isInteger(node.backendDOMNodeId) ? `dom:${node.sourceFrameId}:${node.backendDOMNodeId}` : `ax:${node.sourceFrameId}:${node.nodeId}`;
|
|
32585
|
-
entries.push({ key, semantic, line });
|
|
32586
|
-
treeLength += line.length + 1;
|
|
32587
|
-
}
|
|
32588
|
-
if (name && visibleTextLength < options.textLimit && (role === "StaticText" || role === "heading" || BROWSER_INTERACTIVE_ROLES.has(role))) {
|
|
32589
|
-
const remaining = options.textLimit - visibleTextLength;
|
|
32590
|
-
visibleText.push(name.slice(0, remaining));
|
|
32591
|
-
visibleTextLength += Math.min(name.length, remaining) + 1;
|
|
32592
|
-
}
|
|
32593
|
-
}
|
|
32594
|
-
const revision = createHash3("sha256").update(JSON.stringify([page.url, entries.map(({ key, semantic }) => [key, semantic])])).digest("hex").slice(0, 16);
|
|
32595
|
-
const snapshot = {
|
|
32596
|
-
title: (page.title ?? "").slice(0, 1e3),
|
|
32597
|
-
url: (page.url ?? "").slice(0, 2e3),
|
|
32598
|
-
documentId: browserDocumentId(frameTree),
|
|
32599
|
-
revision,
|
|
32600
|
-
viewport,
|
|
32601
|
-
text: visibleText.join(" ").slice(0, options.textLimit),
|
|
32602
|
-
tree: entries.map(({ line }) => line).join("\n"),
|
|
32603
|
-
elements,
|
|
32604
|
-
controls: elements,
|
|
32605
|
-
nodeCount: entries.length,
|
|
32606
|
-
elementCount: elements.length,
|
|
32607
|
-
truncated: entries.length < meaningful.length || elements.length < meaningful.filter(({ node, role }) => browserNodeActionable(role, propertyMap(node))).length
|
|
32608
|
-
};
|
|
32609
|
-
return { snapshot, entries };
|
|
32610
|
-
}
|
|
32611
|
-
async function computerBrowserCommand(options = {}) {
|
|
32612
|
-
const textLimit = options.limit ? parseCoord(options.limit, "--limit") : 4e3;
|
|
32613
|
-
const elementLimit = options.elementLimit ? parseCoord(options.elementLimit, "--element-limit") : 80;
|
|
32614
|
-
if (textLimit < 0 || textLimit > 5e4) fail("--limit must be between 0 and 50000");
|
|
32615
|
-
if (elementLimit < 0 || elementLimit > 500) fail("--element-limit must be between 0 and 500");
|
|
32616
|
-
const targets = options.targetId || options.page || options.title || options.url ? [await selectChromePage(options)] : await getChromePages();
|
|
32617
|
-
const pages = targets.map((target) => ({
|
|
32618
|
-
target,
|
|
32619
|
-
page: {
|
|
32620
|
-
id: target.id ?? null,
|
|
32621
|
-
title: target.title ?? "",
|
|
32622
|
-
url: target.url ?? "",
|
|
32623
|
-
attached: !!target.attached
|
|
32624
|
-
}
|
|
32625
|
-
}));
|
|
32626
|
-
const pageResults = [];
|
|
32627
|
-
for (const { target, page } of pages) {
|
|
32628
|
-
if (options.snapshot && target.webSocketDebuggerUrl) {
|
|
32629
|
-
try {
|
|
32630
|
-
const { snapshot } = await captureBrowserSnapshot(
|
|
32631
|
-
{ ...target, webSocketDebuggerUrl: target.webSocketDebuggerUrl },
|
|
32632
|
-
{ textLimit, elementLimit }
|
|
32633
|
-
);
|
|
32634
|
-
pageResults.push({ ...page, snapshot });
|
|
32635
|
-
} catch (error51) {
|
|
32636
|
-
pageResults.push({ ...page, snapshotError: error51 instanceof Error ? error51.message : "snapshot failed" });
|
|
32637
|
-
}
|
|
32638
|
-
} else {
|
|
32639
|
-
pageResults.push(page);
|
|
32640
|
-
}
|
|
32641
|
-
}
|
|
32642
|
-
console.log(JSON.stringify({
|
|
32643
|
-
port: CHROME_DEBUG_PORT,
|
|
32644
|
-
pageCount: pageResults.length,
|
|
32645
|
-
pages: pageResults
|
|
32646
|
-
}, null, 2));
|
|
32647
|
-
}
|
|
32648
|
-
function buildBrowserStabilityExpression() {
|
|
32649
|
-
return `(() => {
|
|
32650
|
-
const key = '__replicasComputerUseObserver';
|
|
32651
|
-
if (!globalThis[key]) {
|
|
32652
|
-
const state = { revision: 0 };
|
|
32653
|
-
const observer = new MutationObserver(() => state.revision++);
|
|
32654
|
-
globalThis[key] = { state, observer, observing: false };
|
|
32655
|
-
}
|
|
32656
|
-
if (document.documentElement && !globalThis[key].observing) {
|
|
32657
|
-
globalThis[key].observer.observe(document.documentElement, {
|
|
32658
|
-
subtree: true,
|
|
32659
|
-
childList: true,
|
|
32660
|
-
attributes: true,
|
|
32661
|
-
characterData: true,
|
|
32662
|
-
});
|
|
32663
|
-
globalThis[key].observing = true;
|
|
32664
|
-
}
|
|
32665
|
-
const controls = Array.from(document.querySelectorAll('input,textarea,select,[contenteditable="true"],[role="checkbox"],[role="combobox"],[role="slider"],[role="switch"],[role="textbox"]')).slice(0, 200);
|
|
32666
|
-
const active = document.activeElement;
|
|
32667
|
-
const controlState = controls.map((element, index) => {
|
|
32668
|
-
const value = 'value' in element
|
|
32669
|
-
? element instanceof HTMLInputElement && element.type === 'password'
|
|
32670
|
-
? String(element.value).length
|
|
32671
|
-
: String(element.value)
|
|
32672
|
-
: element.textContent;
|
|
32673
|
-
return [
|
|
32674
|
-
index,
|
|
32675
|
-
element === active,
|
|
32676
|
-
value,
|
|
32677
|
-
'checked' in element ? element.checked : null,
|
|
32678
|
-
'selectedIndex' in element ? element.selectedIndex : null,
|
|
32679
|
-
'disabled' in element ? element.disabled : null,
|
|
32680
|
-
element.getAttribute('aria-checked'),
|
|
32681
|
-
element.getAttribute('aria-expanded'),
|
|
32682
|
-
element.getAttribute('aria-selected'),
|
|
32683
|
-
].join('|');
|
|
32684
|
-
}).join('\\u001f');
|
|
32685
|
-
let controlRevision = 2166136261;
|
|
32686
|
-
for (let index = 0; index < controlState.length; index++) {
|
|
32687
|
-
controlRevision ^= controlState.charCodeAt(index);
|
|
32688
|
-
controlRevision = Math.imul(controlRevision, 16777619);
|
|
32689
|
-
}
|
|
32690
|
-
return {
|
|
32691
|
-
revision: globalThis[key].state.revision,
|
|
32692
|
-
controlRevision: (controlRevision >>> 0).toString(16),
|
|
32693
|
-
readyState: document.readyState,
|
|
32694
|
-
busy: !!document.querySelector('[aria-busy="true"]'),
|
|
32695
|
-
url: location.href.slice(0, 2000),
|
|
32696
|
-
scrollX: Math.round(scrollX),
|
|
32697
|
-
scrollY: Math.round(scrollY),
|
|
32698
|
-
innerWidth,
|
|
32699
|
-
innerHeight,
|
|
32700
|
-
devicePixelRatio,
|
|
32701
|
-
};
|
|
32702
|
-
})()`;
|
|
32703
|
-
}
|
|
32704
|
-
async function waitForBrowserStability(page, options) {
|
|
32705
|
-
const startedAt = Date.now();
|
|
32706
|
-
let lastSignature = null;
|
|
32707
|
-
let lastChangeAt = startedAt;
|
|
32708
|
-
let samples = 0;
|
|
32709
|
-
let changes = 0;
|
|
32710
|
-
let state = null;
|
|
32711
|
-
while (Date.now() - startedAt <= options.timeoutMs) {
|
|
32712
|
-
try {
|
|
32713
|
-
state = await evaluateChromeTarget(page.webSocketDebuggerUrl, buildBrowserStabilityExpression());
|
|
32714
|
-
samples++;
|
|
32715
|
-
const signature = JSON.stringify(state);
|
|
32716
|
-
const now = Date.now();
|
|
32717
|
-
if (lastSignature === null || signature !== lastSignature) {
|
|
32718
|
-
if (lastSignature !== null) changes++;
|
|
32719
|
-
lastSignature = signature;
|
|
32720
|
-
lastChangeAt = now;
|
|
32721
|
-
}
|
|
32722
|
-
const ready = typeof state === "object" && state !== null && "readyState" in state && state.readyState !== "loading" && "busy" in state && state.busy !== true;
|
|
32723
|
-
if (samples > 1 && ready && now - lastChangeAt >= options.stableMs) {
|
|
32724
|
-
return { stable: true, elapsedMs: now - startedAt, samples, changes, state };
|
|
32725
|
-
}
|
|
32726
|
-
} catch {
|
|
32727
|
-
}
|
|
32728
|
-
await sleep2(options.pollMs);
|
|
32729
|
-
}
|
|
32730
|
-
return { stable: false, elapsedMs: Date.now() - startedAt, samples, changes, state };
|
|
32731
|
-
}
|
|
32732
|
-
function browserStateCachePath(targetId) {
|
|
32733
|
-
return `${STATE_DIR}/browser-state-${targetId.replace(/[^a-zA-Z0-9_-]/g, "_")}.json`;
|
|
32734
|
-
}
|
|
32735
|
-
function browserStateDiff(previous, current) {
|
|
32736
|
-
const before = new Map(previous.map((entry) => [entry.key, entry]));
|
|
32737
|
-
const after = new Map(current.map((entry) => [entry.key, entry]));
|
|
32738
|
-
const lines = [];
|
|
32739
|
-
let added = 0;
|
|
32740
|
-
let changed = 0;
|
|
32741
|
-
let removed = 0;
|
|
32742
|
-
for (const entry of current) {
|
|
32743
|
-
const old = before.get(entry.key);
|
|
32744
|
-
if (!old) {
|
|
32745
|
-
added++;
|
|
32746
|
-
lines.push(`+ ${entry.line}`);
|
|
32747
|
-
} else if (old.semantic !== entry.semantic) {
|
|
32748
|
-
changed++;
|
|
32749
|
-
lines.push(`~ ${old.line}
|
|
32750
|
-
${entry.line}`);
|
|
32751
|
-
}
|
|
32752
|
-
}
|
|
32753
|
-
for (const entry of previous) {
|
|
32754
|
-
if (after.has(entry.key)) continue;
|
|
32755
|
-
removed++;
|
|
32756
|
-
lines.push(`- ${entry.line}`);
|
|
32757
|
-
}
|
|
32758
|
-
return { tree: lines.join("\n") || "No accessibility changes.", added, changed, removed };
|
|
32759
|
-
}
|
|
32760
|
-
async function computerBrowserStateCommand(path6, options = {}) {
|
|
32761
|
-
const textLimit = options.limit ? parseCoord(options.limit, "--limit") : 8e3;
|
|
32762
|
-
const elementLimit = options.elementLimit ? parseCoord(options.elementLimit, "--element-limit") : 120;
|
|
32763
|
-
const timeoutMs = options.timeout ? parseCoord(options.timeout, "--timeout") : 5e3;
|
|
32764
|
-
const stableMs = options.stableMs ? parseCoord(options.stableMs, "--stable-ms") : 500;
|
|
32765
|
-
const pollMs = options.pollMs ? parseCoord(options.pollMs, "--poll-ms") : 100;
|
|
32766
|
-
if (textLimit < 0 || textLimit > 5e4) fail("--limit must be between 0 and 50000");
|
|
32767
|
-
if (elementLimit < 0 || elementLimit > 500) fail("--element-limit must be between 0 and 500");
|
|
32768
|
-
if (timeoutMs < 0) fail("--timeout must be >= 0");
|
|
32769
|
-
if (stableMs < 0) fail("--stable-ms must be >= 0");
|
|
32770
|
-
if (pollMs < 50 || pollMs > 2e3) fail("--poll-ms must be between 50 and 2000");
|
|
32771
|
-
const page = await selectChromePage(options);
|
|
32772
|
-
const stability = await waitForBrowserStability(page, { timeoutMs, stableMs, pollMs });
|
|
32773
|
-
const target = resolvePath(path6);
|
|
32774
|
-
mkdirSync5(dirname3(target), { recursive: true });
|
|
32775
|
-
const [{ snapshot, entries }, screenshotResult] = await Promise.all([
|
|
32776
|
-
captureBrowserSnapshot(page, { textLimit, elementLimit }),
|
|
32777
|
-
sendChromeCommand(page.webSocketDebuggerUrl, "Page.captureScreenshot", {
|
|
32778
|
-
format: "png",
|
|
32779
|
-
fromSurface: true,
|
|
32780
|
-
captureBeyondViewport: false
|
|
32781
|
-
})
|
|
32782
|
-
]);
|
|
32783
|
-
const data = screenshotResult.data;
|
|
32784
|
-
if (typeof data !== "string") fail("Chrome did not return screenshot data");
|
|
32785
|
-
writeFileSync5(target, Buffer.from(data, "base64"));
|
|
32786
|
-
const screenshot = readPngDimensions(target);
|
|
32787
|
-
const targetId = page.id ?? "unknown";
|
|
32788
|
-
const cachePath = browserStateCachePath(targetId);
|
|
32789
|
-
let mode = "full";
|
|
32790
|
-
let tree = snapshot.tree;
|
|
32791
|
-
let changes = { added: snapshot.nodeCount, changed: 0, removed: 0 };
|
|
32792
|
-
if (!options.full && existsSync3(cachePath)) {
|
|
32793
|
-
const cached2 = readBrowserStateCache(cachePath);
|
|
32794
|
-
if (cached2?.url === snapshot.url && cached2.documentId === snapshot.documentId) {
|
|
32795
|
-
mode = "diff";
|
|
32796
|
-
const diff = browserStateDiff(cached2.entries, entries);
|
|
32797
|
-
tree = diff.tree;
|
|
32798
|
-
changes = { added: diff.added, changed: diff.changed, removed: diff.removed };
|
|
32799
|
-
}
|
|
32800
|
-
}
|
|
32801
|
-
mkdirSync5(STATE_DIR, { recursive: true });
|
|
32802
|
-
writeFileSync5(cachePath, JSON.stringify({ url: snapshot.url, documentId: snapshot.documentId, entries }));
|
|
32803
|
-
const stableState = isRecord(stability.state) ? stability.state : {};
|
|
32804
|
-
const state = {
|
|
32805
|
-
title: snapshot.title,
|
|
32806
|
-
url: snapshot.url,
|
|
32807
|
-
documentId: snapshot.documentId,
|
|
32808
|
-
revision: snapshot.revision,
|
|
32809
|
-
viewport: snapshot.viewport,
|
|
32810
|
-
text: snapshot.text,
|
|
32811
|
-
tree,
|
|
32812
|
-
nodeCount: snapshot.nodeCount,
|
|
32813
|
-
elementCount: snapshot.elementCount,
|
|
32814
|
-
truncated: snapshot.truncated,
|
|
32815
|
-
mode,
|
|
32816
|
-
changes
|
|
32817
|
-
};
|
|
32818
|
-
console.log(JSON.stringify({
|
|
32819
|
-
targetId,
|
|
32820
|
-
screenshot: {
|
|
32821
|
-
path: target,
|
|
32822
|
-
width: screenshot.width,
|
|
32823
|
-
height: screenshot.height,
|
|
32824
|
-
devicePixelRatio: typeof stableState.devicePixelRatio === "number" ? stableState.devicePixelRatio : 1
|
|
32825
|
-
},
|
|
32826
|
-
stability,
|
|
32827
|
-
state
|
|
32828
|
-
}, null, 2));
|
|
32829
|
-
}
|
|
32830
|
-
var DESKTOP_POINT_JS = `
|
|
32831
|
-
const desktopPoint = (rect) => {
|
|
32832
|
-
const borderX = Math.max(0, (window.outerWidth - window.innerWidth) / 2);
|
|
32833
|
-
const topChrome = Math.max(0, window.outerHeight - window.innerHeight - borderX);
|
|
32834
|
-
return {
|
|
32835
|
-
x: Math.round(window.screenX + borderX + rect.x + rect.width / 2),
|
|
32836
|
-
y: Math.round(window.screenY + topChrome + rect.y + rect.height / 2),
|
|
32837
|
-
};
|
|
32838
|
-
};`;
|
|
32839
|
-
function parseBrowserRef(value) {
|
|
32840
|
-
const ref = parseCoord(value.replace(/^ref=/, ""), "--ref");
|
|
32841
|
-
if (ref <= 0) fail("--ref must be a positive backend DOM node ID from the latest browser state");
|
|
32842
|
-
return ref;
|
|
32843
|
-
}
|
|
32844
|
-
function readBrowserRefCache(page) {
|
|
32845
|
-
const targetId = page.id;
|
|
32846
|
-
if (!targetId) fail("Chrome target has no stable ID. Capture fresh browser state and retry.");
|
|
32847
|
-
const cachePath = browserStateCachePath(targetId);
|
|
32848
|
-
if (!existsSync3(cachePath)) fail(`No browser state is cached for target ${targetId}. Run replicas computer browser-state <path> --target-id ${targetId} first.`);
|
|
32849
|
-
const cached2 = readBrowserStateCache(cachePath);
|
|
32850
|
-
if (!cached2) fail(`Browser state for target ${targetId} is invalid. Capture fresh browser state and retry.`);
|
|
32851
|
-
if (cached2.url !== (page.url ?? "").slice(0, 2e3)) fail("Cached browser state belongs to a different URL. Capture fresh browser state and retry.");
|
|
32852
|
-
return { targetId, cached: cached2 };
|
|
32853
|
-
}
|
|
32854
|
-
function browserViewportRect(layout) {
|
|
32855
|
-
const viewport = chromeVisualViewport(layout);
|
|
32856
|
-
return {
|
|
32857
|
-
x: 0,
|
|
32858
|
-
y: 0,
|
|
32859
|
-
width: viewport.width,
|
|
32860
|
-
height: viewport.height,
|
|
32861
|
-
centerX: viewport.width / 2,
|
|
32862
|
-
centerY: viewport.height / 2
|
|
32863
|
-
};
|
|
32864
|
-
}
|
|
32865
|
-
async function prepareBrowserElement(page, refValue) {
|
|
32866
|
-
const backendNodeId = parseBrowserRef(refValue);
|
|
32867
|
-
const { cached: cached2 } = readBrowserRefCache(page);
|
|
32868
|
-
return await withChromeSession(page.webSocketDebuggerUrl, async (session) => {
|
|
32869
|
-
const layout = await sendChromeSessionCommand(session, "Page.getLayoutMetrics", {});
|
|
32870
|
-
const rect = await prepareBrowserRefInSession(session, backendNodeId, cached2, browserViewportRect(layout));
|
|
32871
|
-
const axNodes = (await sendChromeSessionCommand(session, "Accessibility.getPartialAXTree", {
|
|
32872
|
-
backendNodeId,
|
|
32873
|
-
fetchRelatives: false
|
|
32874
|
-
})).nodes;
|
|
32875
|
-
const rawNode = Array.isArray(axNodes) && isRawAXNode(axNodes[0]) ? axNodes[0] : null;
|
|
32876
|
-
const states = rawNode ? browserStateProperties(rawNode) : {};
|
|
32877
|
-
const role = clippedText(rawNode?.role?.value, 80);
|
|
32878
|
-
const element = rawNode ? {
|
|
32879
|
-
ref: String(backendNodeId),
|
|
32880
|
-
role,
|
|
32881
|
-
name: clippedText(rawNode.name?.value) || null,
|
|
32882
|
-
description: clippedText(rawNode.description?.value) || null,
|
|
32883
|
-
value: typeof rawNode.value?.value === "string" ? clippedText(rawNode.value.value) : typeof rawNode.value?.value === "number" || typeof rawNode.value?.value === "boolean" ? rawNode.value.value : null,
|
|
32884
|
-
states,
|
|
32885
|
-
actions: [
|
|
32886
|
-
"click",
|
|
32887
|
-
...["textbox", "searchbox", "combobox", "spinbutton", "slider"].includes(role) || states.editable === true ? ["set_value"] : [],
|
|
32888
|
-
"scroll"
|
|
32889
|
-
]
|
|
32890
|
-
} : null;
|
|
32891
|
-
const desktopPoint = await evaluateChromeSession(session, `(() => {
|
|
32892
|
-
${DESKTOP_POINT_JS}
|
|
32893
|
-
return desktopPoint({ x: ${rect.centerX}, y: ${rect.centerY}, width: 0, height: 0 });
|
|
32894
|
-
})()`);
|
|
32895
|
-
return {
|
|
32896
|
-
ref: String(backendNodeId),
|
|
32897
|
-
rect,
|
|
32898
|
-
element,
|
|
32899
|
-
desktopPoint: typeof desktopPoint === "object" && desktopPoint !== null && "x" in desktopPoint && typeof desktopPoint.x === "number" && "y" in desktopPoint && typeof desktopPoint.y === "number" ? { x: desktopPoint.x, y: desktopPoint.y } : null
|
|
32900
|
-
};
|
|
32901
|
-
});
|
|
32902
|
-
}
|
|
32903
|
-
async function prepareBrowserRefInSession(session, backendNodeId, cached2, viewportRect) {
|
|
32904
|
-
if (!cached2.entries.some(({ line }) => line.trimStart().startsWith(`[ref=${backendNodeId}]`))) {
|
|
32905
|
-
fail(`Browser element ref ${backendNodeId} is not in the latest state. Capture fresh browser state and retry.`);
|
|
32906
|
-
}
|
|
32907
|
-
const frameTree = (await sendChromeSessionCommand(session, "Page.getFrameTree", {})).frameTree;
|
|
32908
|
-
if (cached2.documentId !== browserDocumentId(frameTree)) {
|
|
32909
|
-
fail(`Browser element ref ${backendNodeId} belongs to a previous document. Capture fresh browser state and retry.`);
|
|
32910
|
-
}
|
|
32911
|
-
try {
|
|
32912
|
-
await sendChromeSessionCommand(session, "DOM.scrollIntoViewIfNeeded", { backendNodeId });
|
|
32913
|
-
} catch (error51) {
|
|
32914
|
-
fail(`Browser element ref ${backendNodeId} is stale or cannot be scrolled into view: ${error51 instanceof Error ? error51.message : String(error51)}`);
|
|
32915
|
-
}
|
|
32916
|
-
const quads = (await sendChromeSessionCommand(session, "DOM.getContentQuads", { backendNodeId })).quads;
|
|
32917
|
-
const candidates = browserContentQuadRects(quads, viewportRect);
|
|
32918
|
-
if (!candidates[0]) fail(`Browser element ref ${backendNodeId} has no visible content quad. Fetch fresh browser state and retry.`);
|
|
32919
|
-
return roundBrowserRect(candidates[0]);
|
|
32920
|
-
}
|
|
32921
|
-
function buildBrowserClickExpression(query, options) {
|
|
32922
|
-
return `(() => {
|
|
32923
|
-
const query = ${JSON.stringify(query)};
|
|
32924
|
-
const exact = ${JSON.stringify(options.exact)};
|
|
32925
|
-
const index = ${options.index};
|
|
32926
|
-
const normalize = (value) => String(value || '').replace(/\\s+/g, ' ').trim();
|
|
32927
|
-
${VISIBLE_JS}
|
|
32928
|
-
const label = (el) => normalize(el.innerText || el.value || el.getAttribute('aria-label') || el.getAttribute('title') || el.getAttribute('placeholder') || el.href || el.id || el.name || el.tagName);
|
|
32929
|
-
${DESKTOP_POINT_JS}
|
|
32930
|
-
const matches = (text) => {
|
|
32931
|
-
const haystack = text.toLowerCase();
|
|
32932
|
-
const needle = query.toLowerCase();
|
|
32933
|
-
return exact ? haystack === needle : haystack.includes(needle);
|
|
32934
|
-
};
|
|
32935
|
-
const selector = 'a,button,input,textarea,select,[role="button"],[role="link"],[role="textbox"],[contenteditable="true"]';
|
|
32936
|
-
const candidates = Array.from(document.querySelectorAll(selector))
|
|
32937
|
-
.filter(visible)
|
|
32938
|
-
.map((el) => ({ el, text: label(el) }))
|
|
32939
|
-
.filter((candidate) => candidate.text && matches(candidate.text));
|
|
32940
|
-
const candidate = candidates[index];
|
|
32941
|
-
if (!candidate) {
|
|
32942
|
-
return {
|
|
32943
|
-
clicked: false,
|
|
32944
|
-
query,
|
|
32945
|
-
matchCount: candidates.length,
|
|
32946
|
-
reason: candidates.length ? 'index out of range' : 'no visible matching control',
|
|
32947
|
-
};
|
|
32948
|
-
}
|
|
32949
|
-
const { el, text } = candidate;
|
|
32950
|
-
el.scrollIntoView({ block: 'center', inline: 'center' });
|
|
32951
|
-
const rect = el.getBoundingClientRect();
|
|
32952
|
-
if (typeof el.focus === 'function') el.focus({ preventScroll: true });
|
|
32953
|
-
return {
|
|
32954
|
-
clicked: true,
|
|
32955
|
-
query,
|
|
32956
|
-
matchCount: candidates.length,
|
|
32957
|
-
index,
|
|
32958
|
-
tag: el.tagName.toLowerCase(),
|
|
32959
|
-
role: el.getAttribute('role') || null,
|
|
32960
|
-
type: el.getAttribute('type') || null,
|
|
32961
|
-
text,
|
|
32962
|
-
href: el.href || null,
|
|
32963
|
-
desktopPoint: desktopPoint(rect),
|
|
32964
|
-
rect: {
|
|
32965
|
-
x: Math.round(rect.x),
|
|
32966
|
-
y: Math.round(rect.y),
|
|
32967
|
-
width: Math.round(rect.width),
|
|
32968
|
-
height: Math.round(rect.height),
|
|
32969
|
-
centerX: Math.round(rect.x + rect.width / 2),
|
|
32970
|
-
centerY: Math.round(rect.y + rect.height / 2),
|
|
32971
|
-
},
|
|
32972
|
-
};
|
|
32973
|
-
})()`;
|
|
32974
|
-
}
|
|
32975
|
-
function browserActionRectCenter(result) {
|
|
32976
|
-
if (typeof result !== "object" || result === null || !("rect" in result)) return null;
|
|
32977
|
-
const rect = result.rect;
|
|
32978
|
-
if (typeof rect !== "object" || rect === null) return null;
|
|
32979
|
-
const x = "centerX" in rect ? rect.centerX : void 0;
|
|
32980
|
-
const y = "centerY" in rect ? rect.centerY : void 0;
|
|
32981
|
-
if (typeof x !== "number" || typeof y !== "number" || !Number.isFinite(x) || !Number.isFinite(y)) return null;
|
|
32982
|
-
return { x, y };
|
|
32983
|
-
}
|
|
32984
|
-
function browserActionPoint(result) {
|
|
32985
|
-
if (typeof result !== "object" || result === null) return null;
|
|
32986
|
-
const action = "result" in result ? result.result : result;
|
|
32987
|
-
if (typeof action !== "object" || action === null || !("desktopPoint" in action)) return null;
|
|
32988
|
-
const point = action.desktopPoint;
|
|
32989
|
-
if (typeof point !== "object" || point === null) return null;
|
|
32990
|
-
const x = "x" in point ? point.x : void 0;
|
|
32991
|
-
const y = "y" in point ? point.y : void 0;
|
|
32992
|
-
if (typeof x !== "number" || typeof y !== "number" || !Number.isFinite(x) || !Number.isFinite(y)) return null;
|
|
32993
|
-
const dimensions = getDisplayDimensions();
|
|
32994
|
-
return {
|
|
32995
|
-
x: clamp(Math.round(x), 0, dimensions.width),
|
|
32996
|
-
y: clamp(Math.round(y), 0, dimensions.height)
|
|
32997
|
-
};
|
|
32998
|
-
}
|
|
32999
|
-
async function dispatchChromeClick(page, x, y, button, clicks) {
|
|
33000
|
-
await withChromeSession(page.webSocketDebuggerUrl, async (session) => {
|
|
33001
|
-
await dispatchChromeClickInSession(session, x, y, button, clicks);
|
|
33002
|
-
});
|
|
33003
|
-
}
|
|
33004
|
-
async function dispatchChromeClickInSession(session, x, y, button, clicks) {
|
|
33005
|
-
for (let clickCount = 1; clickCount <= clicks; clickCount++) {
|
|
33006
|
-
await sendChromeSessionCommand(session, "Input.dispatchMouseEvent", {
|
|
33007
|
-
type: "mousePressed",
|
|
33008
|
-
x,
|
|
33009
|
-
y,
|
|
33010
|
-
button,
|
|
33011
|
-
clickCount
|
|
33012
|
-
});
|
|
33013
|
-
await sendChromeSessionCommand(session, "Input.dispatchMouseEvent", {
|
|
33014
|
-
type: "mouseReleased",
|
|
33015
|
-
x,
|
|
33016
|
-
y,
|
|
33017
|
-
button,
|
|
33018
|
-
clickCount
|
|
33019
|
-
});
|
|
33020
|
-
}
|
|
33021
|
-
}
|
|
33022
|
-
async function replaceChromeText(page, text) {
|
|
33023
|
-
await withChromeSession(page.webSocketDebuggerUrl, async (session) => {
|
|
33024
|
-
await replaceChromeTextInSession(session, text);
|
|
33025
|
-
});
|
|
33026
|
-
}
|
|
33027
|
-
async function replaceChromeTextInSession(session, text) {
|
|
33028
|
-
await sendChromeSessionCommand(session, "Input.dispatchKeyEvent", {
|
|
33029
|
-
type: "rawKeyDown",
|
|
33030
|
-
key: "a",
|
|
33031
|
-
code: "KeyA",
|
|
33032
|
-
windowsVirtualKeyCode: 65,
|
|
33033
|
-
nativeVirtualKeyCode: 65,
|
|
33034
|
-
modifiers: 2
|
|
33035
|
-
});
|
|
33036
|
-
await sendChromeSessionCommand(session, "Input.dispatchKeyEvent", {
|
|
33037
|
-
type: "keyUp",
|
|
33038
|
-
key: "a",
|
|
33039
|
-
code: "KeyA",
|
|
33040
|
-
windowsVirtualKeyCode: 65,
|
|
33041
|
-
nativeVirtualKeyCode: 65,
|
|
33042
|
-
modifiers: 2
|
|
33043
|
-
});
|
|
33044
|
-
await sendChromeSessionCommand(session, "Input.insertText", { text });
|
|
33045
|
-
}
|
|
33046
|
-
var BROWSER_DIRECT_VALUE_INPUT_TYPES = /* @__PURE__ */ new Set(["number", "range", "date", "datetime-local", "month", "week", "time", "color"]);
|
|
33047
|
-
function browserFillInvalid(actual, expected, fieldType, semanticSelection = false, selectionMatched = false) {
|
|
33048
|
-
if (semanticSelection) return !selectionMatched;
|
|
33049
|
-
if (["number", "range"].includes(fieldType)) {
|
|
33050
|
-
const actualNumber = typeof actual === "number" ? actual : Number(actual);
|
|
33051
|
-
const expectedNumber = Number(expected);
|
|
33052
|
-
return !Number.isFinite(actualNumber) || !Number.isFinite(expectedNumber) || actualNumber !== expectedNumber;
|
|
33053
|
-
}
|
|
33054
|
-
if (fieldType === "color") return String(actual).toLowerCase() !== expected.toLowerCase();
|
|
33055
|
-
return actual !== expected;
|
|
33056
|
-
}
|
|
33057
|
-
async function fillChromeNodeInSession(session, backendNodeId, value) {
|
|
33058
|
-
await sendChromeSessionCommand(session, "DOM.focus", { backendNodeId });
|
|
33059
|
-
const directResult = await callFunctionOnChromeNodeInSession(
|
|
33060
|
-
session,
|
|
33061
|
-
backendNodeId,
|
|
33062
|
-
`function(value) {
|
|
33063
|
-
const emit = () => {
|
|
33064
|
-
this.dispatchEvent(new Event('input', { bubbles: true }));
|
|
33065
|
-
this.dispatchEvent(new Event('change', { bubbles: true }));
|
|
33066
|
-
};
|
|
33067
|
-
if (this instanceof HTMLSelectElement) {
|
|
33068
|
-
const option = Array.from(this.options).find((item) => item.value === value || item.text.trim() === value);
|
|
33069
|
-
if (option) this.value = option.value;
|
|
33070
|
-
else this.value = value;
|
|
33071
|
-
emit();
|
|
33072
|
-
return { handled: true, semanticSelection: true, matched: !!option, value: this.value };
|
|
33073
|
-
}
|
|
33074
|
-
if (this instanceof HTMLInputElement && ${JSON.stringify([...BROWSER_DIRECT_VALUE_INPUT_TYPES])}.includes(this.type.toLowerCase())) {
|
|
33075
|
-
const setter = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value')?.set;
|
|
33076
|
-
if (!setter) return { handled: false };
|
|
33077
|
-
setter.call(this, value);
|
|
33078
|
-
emit();
|
|
33079
|
-
return { handled: true, semanticSelection: false, matched: true, value: this.value };
|
|
33080
|
-
}
|
|
33081
|
-
return { handled: false };
|
|
33082
|
-
}`,
|
|
33083
|
-
[value]
|
|
33084
|
-
);
|
|
33085
|
-
const directValue = chromeCallValue(directResult);
|
|
33086
|
-
const directHandled = isRecord(directValue) && directValue.handled === true;
|
|
33087
|
-
if (!directHandled) await replaceChromeTextInSession(session, value);
|
|
33088
|
-
const verification = await callFunctionOnChromeNodeInSession(
|
|
33089
|
-
session,
|
|
33090
|
-
backendNodeId,
|
|
33091
|
-
`function() {
|
|
33092
|
-
const value = this instanceof HTMLSelectElement || 'value' in this ? this.value : this.textContent;
|
|
33093
|
-
const type = this instanceof HTMLSelectElement
|
|
33094
|
-
? 'select'
|
|
33095
|
-
: this instanceof HTMLInputElement
|
|
33096
|
-
? this.type.toLowerCase()
|
|
33097
|
-
: this.isContentEditable
|
|
33098
|
-
? 'contenteditable'
|
|
33099
|
-
: 'text';
|
|
33100
|
-
return { value, valueLength: String(value ?? '').length, type };
|
|
33101
|
-
}`
|
|
33102
|
-
);
|
|
33103
|
-
const actual = chromeCallValue(verification);
|
|
33104
|
-
if (!isRecord(actual)) fail(`Chrome did not return a value for browser element ref ${backendNodeId}.`);
|
|
33105
|
-
const fieldType = typeof actual.type === "string" ? actual.type : "";
|
|
33106
|
-
const semanticSelection = isRecord(directValue) && directValue.semanticSelection === true;
|
|
33107
|
-
const selectionMatched = isRecord(directValue) && directValue.matched === true;
|
|
33108
|
-
if (browserFillInvalid(actual.value, value, fieldType, semanticSelection, selectionMatched)) {
|
|
33109
|
-
fail(`Chrome reported that ref ${backendNodeId} contains ${JSON.stringify(actual.value)} after filling, expected ${JSON.stringify(value)}.`);
|
|
33110
|
-
}
|
|
33111
|
-
return { value: actual.value, valueLength: actual.valueLength, type: fieldType };
|
|
33112
|
-
}
|
|
33113
|
-
async function computerBrowserClickCommand(query, options = {}) {
|
|
33114
|
-
const page = await selectChromePage(options);
|
|
33115
|
-
if (options.ref) {
|
|
33116
|
-
const target = await prepareBrowserElement(page, options.ref);
|
|
33117
|
-
const buttonNames = { "1": "left", "2": "middle", "3": "right", left: "left", middle: "middle", right: "right" };
|
|
33118
|
-
const button = buttonNames[(options.button ?? "left").toLowerCase()];
|
|
33119
|
-
if (!button) fail("--button must be one of left|middle|right|1|2|3");
|
|
33120
|
-
const clicks = options.double ? 2 : 1;
|
|
33121
|
-
await dispatchChromeClick(page, target.rect.centerX, target.rect.centerY, button, clicks);
|
|
33122
|
-
if (target.desktopPoint) logRecordingAction({ type: "click", ...target.desktopPoint });
|
|
33123
|
-
console.log(JSON.stringify({
|
|
33124
|
-
...browserTargetOutput(page),
|
|
33125
|
-
result: { clicked: true, ref: target.ref, button, clickCount: clicks, rect: target.rect, element: target.element }
|
|
33126
|
-
}, null, 2));
|
|
33127
|
-
return;
|
|
33128
|
-
}
|
|
33129
|
-
if (!query) fail("Provide control text or --ref <id> from the latest browser state");
|
|
33130
|
-
const index = options.index ? parseCoord(options.index, "--index") : 0;
|
|
33131
|
-
if (index < 0) fail("--index must be >= 0");
|
|
33132
|
-
const result = await evaluateChromeTarget(
|
|
33133
|
-
page.webSocketDebuggerUrl,
|
|
33134
|
-
buildBrowserClickExpression(query, { exact: !!options.exact, index })
|
|
33135
|
-
);
|
|
33136
|
-
const center = browserActionRectCenter(result);
|
|
33137
|
-
if (center) await dispatchChromeClick(page, center.x, center.y, "left", 1);
|
|
33138
|
-
const point = browserActionPoint(result);
|
|
33139
|
-
if (point) logRecordingAction({ type: "click", ...point });
|
|
33140
|
-
console.log(JSON.stringify({
|
|
33141
|
-
...browserTargetOutput(page),
|
|
33142
|
-
result
|
|
33143
|
-
}, null, 2));
|
|
33144
|
-
}
|
|
33145
|
-
function buildBrowserFillExpression(query, value, options) {
|
|
33146
|
-
return `(() => {
|
|
33147
|
-
const query = ${JSON.stringify(query)};
|
|
33148
|
-
const value = ${JSON.stringify(value)};
|
|
33149
|
-
const exact = ${JSON.stringify(options.exact)};
|
|
33150
|
-
const index = ${options.index};
|
|
33151
|
-
const normalize = (text) => String(text || '').replace(/\\s+/g, ' ').trim();
|
|
33152
|
-
const escape = (text) => globalThis.CSS && CSS.escape ? CSS.escape(text) : String(text).replace(/["\\\\]/g, '\\\\$&');
|
|
33153
|
-
${VISIBLE_JS}
|
|
33154
|
-
const matches = (text) => {
|
|
33155
|
-
const haystack = text.toLowerCase();
|
|
33156
|
-
const needle = query.toLowerCase();
|
|
33157
|
-
return exact ? haystack === needle : haystack.includes(needle);
|
|
33158
|
-
};
|
|
33159
|
-
const labelParts = (el) => {
|
|
33160
|
-
const id = el.getAttribute('id');
|
|
33161
|
-
const labels = [];
|
|
33162
|
-
if (id) labels.push(...Array.from(document.querySelectorAll('label[for="' + escape(id) + '"]')).map((label) => label.innerText));
|
|
33163
|
-
const wrappingLabel = el.closest('label');
|
|
33164
|
-
if (wrappingLabel) labels.push(wrappingLabel.innerText);
|
|
33165
|
-
labels.push(
|
|
33166
|
-
el.getAttribute('aria-label'),
|
|
33167
|
-
el.getAttribute('title'),
|
|
33168
|
-
el.getAttribute('placeholder'),
|
|
33169
|
-
el.getAttribute('name'),
|
|
33170
|
-
el.getAttribute('id'),
|
|
33171
|
-
el.value,
|
|
33172
|
-
el.innerText,
|
|
33173
|
-
);
|
|
33174
|
-
return labels.map(normalize).filter(Boolean);
|
|
33175
|
-
};
|
|
33176
|
-
const labelText = (el) => labelParts(el).join(' ');
|
|
33177
|
-
${DESKTOP_POINT_JS}
|
|
33178
|
-
const fieldMatches = (el) => {
|
|
33179
|
-
const parts = labelParts(el);
|
|
33180
|
-
return exact ? parts.some(matches) : matches(parts.join(' '));
|
|
33181
|
-
};
|
|
33182
|
-
const selector = 'input:not([type="button"]):not([type="submit"]):not([type="reset"]):not([type="checkbox"]):not([type="radio"]),textarea,select,[role="textbox"],[contenteditable="true"]';
|
|
33183
|
-
const candidates = Array.from(document.querySelectorAll(selector))
|
|
33184
|
-
.filter(visible)
|
|
33185
|
-
.map((el) => ({ el, text: labelText(el) }))
|
|
33186
|
-
.filter((candidate) => candidate.text && fieldMatches(candidate.el));
|
|
33187
|
-
const candidate = candidates[index];
|
|
33188
|
-
if (!candidate) {
|
|
33189
|
-
return {
|
|
33190
|
-
filled: false,
|
|
33191
|
-
query,
|
|
33192
|
-
matchCount: candidates.length,
|
|
33193
|
-
reason: candidates.length ? 'index out of range' : 'no visible matching field',
|
|
33194
|
-
};
|
|
33195
|
-
}
|
|
33196
|
-
const { el, text } = candidate;
|
|
33197
|
-
el.scrollIntoView({ block: 'center', inline: 'center' });
|
|
33198
|
-
if (typeof el.focus === 'function') el.focus({ preventScroll: true });
|
|
33199
|
-
const fieldType = el.tagName.toLowerCase() === 'select' ? 'select' : String(el.getAttribute('type') || 'text').toLowerCase();
|
|
33200
|
-
let nativeInputRequired = false;
|
|
33201
|
-
let semanticSelection = false;
|
|
33202
|
-
let selectionMatched = false;
|
|
33203
|
-
if (el.tagName.toLowerCase() === 'select') {
|
|
33204
|
-
const option = Array.from(el.options).find((item) => item.value === value || normalize(item.text) === value);
|
|
33205
|
-
if (option) el.value = option.value;
|
|
33206
|
-
else el.value = value;
|
|
33207
|
-
semanticSelection = true;
|
|
33208
|
-
selectionMatched = !!option;
|
|
33209
|
-
el.dispatchEvent(new Event('input', { bubbles: true }));
|
|
33210
|
-
el.dispatchEvent(new Event('change', { bubbles: true }));
|
|
33211
|
-
} else if (el instanceof HTMLInputElement && ${JSON.stringify([...BROWSER_DIRECT_VALUE_INPUT_TYPES])}.includes(fieldType)) {
|
|
33212
|
-
const setter = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value')?.set;
|
|
33213
|
-
if (setter) {
|
|
33214
|
-
setter.call(el, value);
|
|
33215
|
-
el.dispatchEvent(new Event('input', { bubbles: true }));
|
|
33216
|
-
el.dispatchEvent(new Event('change', { bubbles: true }));
|
|
33217
|
-
} else {
|
|
33218
|
-
nativeInputRequired = true;
|
|
33219
|
-
}
|
|
33220
|
-
} else if (el.isContentEditable) {
|
|
33221
|
-
const selection = getSelection();
|
|
33222
|
-
const range = document.createRange();
|
|
33223
|
-
range.selectNodeContents(el);
|
|
33224
|
-
selection.removeAllRanges();
|
|
33225
|
-
selection.addRange(range);
|
|
33226
|
-
nativeInputRequired = true;
|
|
33227
|
-
} else if (typeof el.select === 'function') {
|
|
33228
|
-
el.select();
|
|
33229
|
-
nativeInputRequired = true;
|
|
33230
|
-
} else {
|
|
33231
|
-
el.setSelectionRange(0, String(el.value || '').length);
|
|
33232
|
-
nativeInputRequired = true;
|
|
33233
|
-
}
|
|
33234
|
-
const rect = el.getBoundingClientRect();
|
|
33235
|
-
return {
|
|
33236
|
-
filled: true,
|
|
33237
|
-
query,
|
|
33238
|
-
matchCount: candidates.length,
|
|
33239
|
-
index,
|
|
33240
|
-
tag: el.tagName.toLowerCase(),
|
|
33241
|
-
role: el.getAttribute('role') || null,
|
|
33242
|
-
type: fieldType,
|
|
33243
|
-
text,
|
|
33244
|
-
actualValue: el.isContentEditable ? el.textContent : 'value' in el ? el.value : null,
|
|
33245
|
-
valueLength: String(el.isContentEditable ? el.textContent : 'value' in el ? el.value : '').length,
|
|
33246
|
-
nativeInputRequired,
|
|
33247
|
-
semanticSelection,
|
|
33248
|
-
selectionMatched,
|
|
33249
|
-
desktopPoint: desktopPoint(rect),
|
|
33250
|
-
rect: {
|
|
33251
|
-
x: Math.round(rect.x),
|
|
33252
|
-
y: Math.round(rect.y),
|
|
33253
|
-
width: Math.round(rect.width),
|
|
33254
|
-
height: Math.round(rect.height),
|
|
33255
|
-
centerX: Math.round(rect.x + rect.width / 2),
|
|
33256
|
-
centerY: Math.round(rect.y + rect.height / 2),
|
|
33257
|
-
},
|
|
33258
|
-
};
|
|
33259
|
-
})()`;
|
|
33260
|
-
}
|
|
33261
|
-
async function computerBrowserFillCommand(query, value, options = {}) {
|
|
33262
|
-
const page = await selectChromePage(options);
|
|
33263
|
-
if (options.ref) {
|
|
33264
|
-
const target = await prepareBrowserElement(page, options.ref);
|
|
33265
|
-
const backendNodeId = parseBrowserRef(options.ref);
|
|
33266
|
-
const actual = await withChromeSession(page.webSocketDebuggerUrl, async (session) => await fillChromeNodeInSession(session, backendNodeId, value));
|
|
33267
|
-
if (target.desktopPoint) logRecordingAction({ type: "type", ...target.desktopPoint });
|
|
33268
|
-
console.log(JSON.stringify({
|
|
33269
|
-
...browserTargetOutput(page),
|
|
33270
|
-
result: { filled: true, ref: target.ref, valueLength: actual.valueLength, rect: target.rect, element: target.element }
|
|
33271
|
-
}, null, 2));
|
|
33272
|
-
return;
|
|
33273
|
-
}
|
|
33274
|
-
if (!query) fail("Provide field text or --ref <id> from the latest browser state");
|
|
33275
|
-
const index = options.index ? parseCoord(options.index, "--index") : 0;
|
|
33276
|
-
if (index < 0) fail("--index must be >= 0");
|
|
33277
|
-
const result = await evaluateChromeTarget(
|
|
33278
|
-
page.webSocketDebuggerUrl,
|
|
33279
|
-
buildBrowserFillExpression(query, value, { exact: !!options.exact, index })
|
|
33280
|
-
);
|
|
33281
|
-
const point = browserActionPoint(result);
|
|
33282
|
-
if (typeof result === "object" && result !== null && "nativeInputRequired" in result && result.nativeInputRequired === true) {
|
|
33283
|
-
await replaceChromeText(page, value);
|
|
33284
|
-
const actualValue = await evaluateChromeTarget(page.webSocketDebuggerUrl, `(() => {
|
|
33285
|
-
const el = document.activeElement;
|
|
33286
|
-
return el && el.isContentEditable ? el.textContent : el && 'value' in el ? el.value : null;
|
|
33287
|
-
})()`);
|
|
33288
|
-
const fieldType = "type" in result && typeof result.type === "string" ? result.type.toLowerCase() : "";
|
|
33289
|
-
if (browserFillInvalid(actualValue, value, fieldType)) {
|
|
33290
|
-
fail(`Chrome reported that ${JSON.stringify(query)} contains ${JSON.stringify(actualValue)} after filling, expected ${JSON.stringify(value)}.`);
|
|
33291
|
-
}
|
|
33292
|
-
if (isRecord(result)) {
|
|
33293
|
-
Object.assign(result, { actualValue, valueLength: String(actualValue ?? "").length });
|
|
33294
|
-
}
|
|
33295
|
-
} else if (isRecord(result) && result.filled === true) {
|
|
33296
|
-
const fieldType = typeof result.type === "string" ? result.type : "";
|
|
33297
|
-
if (browserFillInvalid(result.actualValue, value, fieldType, result.semanticSelection === true, result.selectionMatched === true)) {
|
|
33298
|
-
fail(`Chrome reported that ${JSON.stringify(query)} contains ${JSON.stringify(result.actualValue)} after filling, expected ${JSON.stringify(value)}.`);
|
|
33299
|
-
}
|
|
33300
|
-
}
|
|
33301
|
-
if (point) logRecordingAction({ type: "type", ...point });
|
|
33302
|
-
console.log(JSON.stringify({
|
|
33303
|
-
...browserTargetOutput(page),
|
|
33304
|
-
result
|
|
33305
|
-
}, null, 2));
|
|
33306
|
-
}
|
|
33307
|
-
function buildBrowserWaitExpression(query, options) {
|
|
33308
|
-
return `(() => {
|
|
33309
|
-
const query = ${JSON.stringify(query)};
|
|
33310
|
-
const mode = ${JSON.stringify(options.mode)};
|
|
33311
|
-
const exact = ${JSON.stringify(options.exact)};
|
|
33312
|
-
const normalize = (text) => String(text || '').replace(/\\s+/g, ' ').trim();
|
|
33313
|
-
${VISIBLE_JS}
|
|
33314
|
-
const matches = (text) => {
|
|
33315
|
-
const haystack = normalize(text).toLowerCase();
|
|
33316
|
-
const needle = query.toLowerCase();
|
|
33317
|
-
return exact ? haystack === needle : haystack.includes(needle);
|
|
33318
|
-
};
|
|
33319
|
-
const controlText = (el) => normalize(el.innerText || el.value || el.getAttribute('aria-label') || el.getAttribute('title') || el.getAttribute('placeholder') || el.href || el.id || el.name || el.tagName);
|
|
33320
|
-
const controls = () => Array.from(document.querySelectorAll('a,button,input,textarea,select,[role="button"],[role="link"],[role="textbox"],[contenteditable="true"]'))
|
|
33321
|
-
.filter(visible)
|
|
33322
|
-
.map(controlText)
|
|
33323
|
-
.filter(Boolean);
|
|
33324
|
-
const values = {
|
|
33325
|
-
title: [document.title],
|
|
33326
|
-
url: [location.href],
|
|
33327
|
-
text: [document.body ? document.body.innerText : ''],
|
|
33328
|
-
control: controls(),
|
|
33329
|
-
any: [document.title, location.href, document.body ? document.body.innerText : '', ...controls()],
|
|
33330
|
-
}[mode];
|
|
33331
|
-
if (!values) return { matched: false, query, mode, reason: 'invalid mode' };
|
|
33332
|
-
const match = values.find(matches) || null;
|
|
33333
|
-
return {
|
|
33334
|
-
matched: !!match,
|
|
33335
|
-
query,
|
|
33336
|
-
mode,
|
|
33337
|
-
exact,
|
|
33338
|
-
match: match ? normalize(match).slice(0, 500) : null,
|
|
33339
|
-
title: document.title,
|
|
33340
|
-
url: location.href,
|
|
33341
|
-
};
|
|
33342
|
-
})()`;
|
|
33343
|
-
}
|
|
33344
|
-
async function computerBrowserWaitCommand(query, options = {}) {
|
|
33345
|
-
const mode = options.mode ?? "any";
|
|
33346
|
-
if (!["any", "text", "title", "url", "control"].includes(mode)) fail("--mode must be one of any|text|title|url|control");
|
|
33347
|
-
const timeoutMs = options.timeout ? parseCoord(options.timeout, "--timeout") : 1e4;
|
|
33348
|
-
const pollMs = options.pollMs ? parseCoord(options.pollMs, "--poll-ms") : 250;
|
|
33349
|
-
if (timeoutMs < 0) fail("--timeout must be >= 0");
|
|
33350
|
-
if (pollMs < 50 || pollMs > 2e3) fail("--poll-ms must be between 50 and 2000");
|
|
33351
|
-
const page = await selectChromePage(options);
|
|
33352
|
-
const start = Date.now();
|
|
33353
|
-
const expression = buildBrowserWaitExpression(query, { mode, exact: !!options.exact });
|
|
33354
|
-
let attempts = 0;
|
|
33355
|
-
let lastResult = null;
|
|
33356
|
-
while (Date.now() - start <= timeoutMs) {
|
|
33357
|
-
attempts++;
|
|
33358
|
-
lastResult = await evaluateChromeTarget(page.webSocketDebuggerUrl, expression);
|
|
33359
|
-
if (typeof lastResult === "object" && lastResult !== null && "matched" in lastResult && lastResult.matched) {
|
|
33360
|
-
console.log(JSON.stringify({ ok: true, elapsedMs: Date.now() - start, attempts, result: lastResult }, null, 2));
|
|
33361
|
-
return;
|
|
33362
|
-
}
|
|
33363
|
-
await sleep2(pollMs);
|
|
33364
|
-
}
|
|
33365
|
-
console.log(JSON.stringify({ ok: false, elapsedMs: Date.now() - start, attempts, result: lastResult }, null, 2));
|
|
33366
|
-
process.exitCode = 1;
|
|
33367
|
-
}
|
|
33368
|
-
async function computerBrowserScrollCommand(direction, options = {}) {
|
|
33369
|
-
const page = await selectChromePage(options);
|
|
33370
|
-
const normalized = direction.toLowerCase();
|
|
33371
|
-
if (!["up", "down", "left", "right"].includes(normalized)) fail("direction must be one of up|down|left|right");
|
|
33372
|
-
const amount = options.amount ? parseCoord(options.amount, "--amount") : 600;
|
|
33373
|
-
if (amount <= 0 || amount > 1e4) fail("--amount must be between 1 and 10000 CSS pixels");
|
|
33374
|
-
const layout = await sendChromeCommand(page.webSocketDebuggerUrl, "Page.getLayoutMetrics", {});
|
|
33375
|
-
const viewport = chromeVisualViewport(layout);
|
|
33376
|
-
const target = options.ref ? await prepareBrowserElement(page, options.ref) : null;
|
|
33377
|
-
const x = target?.rect.centerX ?? viewport.width / 2;
|
|
33378
|
-
const y = target?.rect.centerY ?? viewport.height / 2;
|
|
33379
|
-
const deltaX = normalized === "left" ? -amount : normalized === "right" ? amount : 0;
|
|
33380
|
-
const deltaY = normalized === "up" ? -amount : normalized === "down" ? amount : 0;
|
|
33381
|
-
await sendChromeCommand(page.webSocketDebuggerUrl, "Input.dispatchMouseEvent", {
|
|
33382
|
-
type: "mouseWheel",
|
|
33383
|
-
x,
|
|
33384
|
-
y,
|
|
33385
|
-
deltaX,
|
|
33386
|
-
deltaY
|
|
33387
|
-
});
|
|
33388
|
-
logRecordingAction({ type: "scroll", ...target?.desktopPoint ?? {} });
|
|
33389
|
-
console.log(JSON.stringify({
|
|
33390
|
-
...browserTargetOutput(page),
|
|
33391
|
-
result: { scrolled: true, direction: normalized, amount, ref: target?.ref ?? null, x: Math.round(x), y: Math.round(y) }
|
|
33392
|
-
}, null, 2));
|
|
33393
|
-
}
|
|
33394
|
-
function browserKeyDetails(combo) {
|
|
33395
|
-
const tokens = combo.split("+").map((token) => token.trim()).filter(Boolean);
|
|
33396
|
-
if (tokens.length === 0) fail("key combination cannot be empty");
|
|
33397
|
-
let modifiers = 0;
|
|
33398
|
-
for (const token of tokens.slice(0, -1)) {
|
|
33399
|
-
const normalized2 = token.toLowerCase();
|
|
33400
|
-
if (["alt", "option"].includes(normalized2)) modifiers |= 1;
|
|
33401
|
-
else if (["ctrl", "control"].includes(normalized2)) modifiers |= 2;
|
|
33402
|
-
else if (["meta", "cmd", "command", "super"].includes(normalized2)) modifiers |= 4;
|
|
33403
|
-
else if (normalized2 === "shift") modifiers |= 8;
|
|
33404
|
-
else fail(`Unsupported browser key modifier ${JSON.stringify(token)}`);
|
|
33405
|
-
}
|
|
33406
|
-
const requested = tokens[tokens.length - 1];
|
|
33407
|
-
const aliases = {
|
|
33408
|
-
enter: { key: "Enter", code: "Enter", windowsVirtualKeyCode: 13 },
|
|
33409
|
-
return: { key: "Enter", code: "Enter", windowsVirtualKeyCode: 13 },
|
|
33410
|
-
tab: { key: "Tab", code: "Tab", windowsVirtualKeyCode: 9 },
|
|
33411
|
-
escape: { key: "Escape", code: "Escape", windowsVirtualKeyCode: 27 },
|
|
33412
|
-
esc: { key: "Escape", code: "Escape", windowsVirtualKeyCode: 27 },
|
|
33413
|
-
backspace: { key: "Backspace", code: "Backspace", windowsVirtualKeyCode: 8 },
|
|
33414
|
-
delete: { key: "Delete", code: "Delete", windowsVirtualKeyCode: 46 },
|
|
33415
|
-
space: { key: " ", code: "Space", windowsVirtualKeyCode: 32 },
|
|
33416
|
-
up: { key: "ArrowUp", code: "ArrowUp", windowsVirtualKeyCode: 38 },
|
|
33417
|
-
arrowup: { key: "ArrowUp", code: "ArrowUp", windowsVirtualKeyCode: 38 },
|
|
33418
|
-
down: { key: "ArrowDown", code: "ArrowDown", windowsVirtualKeyCode: 40 },
|
|
33419
|
-
arrowdown: { key: "ArrowDown", code: "ArrowDown", windowsVirtualKeyCode: 40 },
|
|
33420
|
-
left: { key: "ArrowLeft", code: "ArrowLeft", windowsVirtualKeyCode: 37 },
|
|
33421
|
-
arrowleft: { key: "ArrowLeft", code: "ArrowLeft", windowsVirtualKeyCode: 37 },
|
|
33422
|
-
right: { key: "ArrowRight", code: "ArrowRight", windowsVirtualKeyCode: 39 },
|
|
33423
|
-
arrowright: { key: "ArrowRight", code: "ArrowRight", windowsVirtualKeyCode: 39 },
|
|
33424
|
-
home: { key: "Home", code: "Home", windowsVirtualKeyCode: 36 },
|
|
33425
|
-
end: { key: "End", code: "End", windowsVirtualKeyCode: 35 },
|
|
33426
|
-
pageup: { key: "PageUp", code: "PageUp", windowsVirtualKeyCode: 33 },
|
|
33427
|
-
pagedown: { key: "PageDown", code: "PageDown", windowsVirtualKeyCode: 34 }
|
|
33428
|
-
};
|
|
33429
|
-
const normalized = requested.toLowerCase();
|
|
33430
|
-
const alias = aliases[normalized];
|
|
33431
|
-
if (alias) return { ...alias, modifiers, ...alias.key === " " && modifiers === 0 ? { text: " " } : {} };
|
|
33432
|
-
if (/^[a-z]$/i.test(requested)) {
|
|
33433
|
-
const upper = requested.toUpperCase();
|
|
33434
|
-
const key = modifiers & 8 ? upper : requested.toLowerCase();
|
|
33435
|
-
return { key, code: `Key${upper}`, windowsVirtualKeyCode: upper.charCodeAt(0), modifiers, ...modifiers === 0 ? { text: key } : {} };
|
|
33436
|
-
}
|
|
33437
|
-
if (/^[0-9]$/.test(requested)) {
|
|
33438
|
-
return { key: requested, code: `Digit${requested}`, windowsVirtualKeyCode: requested.charCodeAt(0), modifiers, ...modifiers === 0 ? { text: requested } : {} };
|
|
33439
|
-
}
|
|
33440
|
-
fail(`Unsupported browser key ${JSON.stringify(requested)}`);
|
|
33441
|
-
}
|
|
33442
|
-
async function computerBrowserKeyCommand(combo, options = {}) {
|
|
33443
|
-
const page = await selectChromePage(options);
|
|
33444
|
-
const key = browserKeyDetails(combo);
|
|
33445
|
-
await withChromeSession(page.webSocketDebuggerUrl, async (session) => {
|
|
33446
|
-
await dispatchChromeKeyInSession(session, key);
|
|
33447
|
-
});
|
|
33448
|
-
logRecordingAction({ type: "key" });
|
|
33449
|
-
console.log(JSON.stringify({ targetId: page.id ?? null, result: { pressed: combo } }, null, 2));
|
|
33450
|
-
}
|
|
33451
|
-
async function dispatchChromeKeyInSession(session, key) {
|
|
33452
|
-
await sendChromeSessionCommand(session, "Input.dispatchKeyEvent", {
|
|
33453
|
-
type: "keyDown",
|
|
33454
|
-
...key,
|
|
33455
|
-
nativeVirtualKeyCode: key.windowsVirtualKeyCode
|
|
33456
|
-
});
|
|
33457
|
-
await sendChromeSessionCommand(session, "Input.dispatchKeyEvent", {
|
|
33458
|
-
type: "keyUp",
|
|
33459
|
-
key: key.key,
|
|
33460
|
-
code: key.code,
|
|
33461
|
-
modifiers: key.modifiers,
|
|
33462
|
-
windowsVirtualKeyCode: key.windowsVirtualKeyCode,
|
|
33463
|
-
nativeVirtualKeyCode: key.windowsVirtualKeyCode
|
|
33464
|
-
});
|
|
33465
|
-
}
|
|
33466
|
-
async function computerBrowserTypeCommand(text, options = {}) {
|
|
33467
|
-
const page = await selectChromePage(options);
|
|
33468
|
-
await sendChromeCommand(page.webSocketDebuggerUrl, "Input.insertText", { text });
|
|
33469
|
-
logRecordingAction({ type: "type" });
|
|
33470
|
-
console.log(JSON.stringify({ targetId: page.id ?? null, result: { typed: true, length: text.length } }, null, 2));
|
|
33471
|
-
}
|
|
33472
|
-
function parseBrowserBatchActions(value) {
|
|
33473
|
-
let input;
|
|
33474
|
-
try {
|
|
33475
|
-
input = JSON.parse(value);
|
|
33476
|
-
} catch (error51) {
|
|
33477
|
-
fail(`Batch actions must be valid JSON: ${error51 instanceof Error ? error51.message : String(error51)}`);
|
|
33478
|
-
}
|
|
33479
|
-
if (!Array.isArray(input) || input.length === 0 || input.length > 100) {
|
|
33480
|
-
fail("Batch actions must be a JSON array containing 1 to 100 actions");
|
|
33481
|
-
}
|
|
33482
|
-
return input.map((item, index) => {
|
|
33483
|
-
if (!isRecord(item) || typeof item.action !== "string") fail(`Batch action ${index} must be an object with an action`);
|
|
33484
|
-
if (item.action === "click") {
|
|
33485
|
-
if (typeof item.ref !== "string" && typeof item.ref !== "number") fail(`Batch click ${index} requires ref`);
|
|
33486
|
-
const buttons = { "1": "left", "2": "middle", "3": "right", left: "left", middle: "middle", right: "right" };
|
|
33487
|
-
const requestedButton = item.button === void 0 ? "left" : String(item.button).toLowerCase();
|
|
33488
|
-
const button = buttons[requestedButton];
|
|
33489
|
-
if (!button) fail(`Batch click ${index} button must be left, middle, or right`);
|
|
33490
|
-
return { action: "click", ref: String(item.ref), button, clicks: item.double === true ? 2 : 1 };
|
|
33491
|
-
}
|
|
33492
|
-
if (item.action === "fill") {
|
|
33493
|
-
if (typeof item.ref !== "string" && typeof item.ref !== "number" || typeof item.value !== "string") {
|
|
33494
|
-
fail(`Batch fill ${index} requires ref and string value`);
|
|
33495
|
-
}
|
|
33496
|
-
return { action: "fill", ref: String(item.ref), value: item.value };
|
|
33497
|
-
}
|
|
33498
|
-
if (item.action === "key") {
|
|
33499
|
-
if (typeof item.combo !== "string") fail(`Batch key ${index} requires combo`);
|
|
33500
|
-
return { action: "key", combo: item.combo };
|
|
33501
|
-
}
|
|
33502
|
-
if (item.action === "type") {
|
|
33503
|
-
if (typeof item.text !== "string") fail(`Batch type ${index} requires text`);
|
|
33504
|
-
return { action: "type", text: item.text };
|
|
33505
|
-
}
|
|
33506
|
-
if (item.action === "scroll") {
|
|
33507
|
-
const directions = { up: "up", down: "down", left: "left", right: "right" };
|
|
33508
|
-
const direction = typeof item.direction === "string" ? directions[item.direction] : void 0;
|
|
33509
|
-
if (!direction) {
|
|
33510
|
-
fail(`Batch scroll ${index} direction must be up, down, left, or right`);
|
|
33511
|
-
}
|
|
33512
|
-
const amount = item.amount === void 0 ? 600 : Number(item.amount);
|
|
33513
|
-
if (!Number.isFinite(amount) || amount <= 0 || amount > 1e4) fail(`Batch scroll ${index} amount must be between 1 and 10000`);
|
|
33514
|
-
if (item.ref !== void 0 && typeof item.ref !== "string" && typeof item.ref !== "number") fail(`Batch scroll ${index} ref is invalid`);
|
|
33515
|
-
return {
|
|
33516
|
-
action: "scroll",
|
|
33517
|
-
direction,
|
|
33518
|
-
amount,
|
|
33519
|
-
...item.ref === void 0 ? {} : { ref: String(item.ref) }
|
|
33520
|
-
};
|
|
33521
|
-
}
|
|
33522
|
-
if (item.action === "wait") {
|
|
33523
|
-
if (typeof item.text !== "string") fail(`Batch wait ${index} requires text`);
|
|
33524
|
-
const mode = item.mode === void 0 ? "any" : String(item.mode);
|
|
33525
|
-
if (!["any", "text", "title", "url", "control"].includes(mode)) fail(`Batch wait ${index} mode is invalid`);
|
|
33526
|
-
const timeoutMs = item.timeoutMs === void 0 ? 1e4 : Number(item.timeoutMs);
|
|
33527
|
-
const pollMs = item.pollMs === void 0 ? 250 : Number(item.pollMs);
|
|
33528
|
-
if (!Number.isFinite(timeoutMs) || timeoutMs < 0) fail(`Batch wait ${index} timeoutMs must be >= 0`);
|
|
33529
|
-
if (!Number.isFinite(pollMs) || pollMs < 50 || pollMs > 2e3) fail(`Batch wait ${index} pollMs must be between 50 and 2000`);
|
|
33530
|
-
return { action: "wait", text: item.text, mode, exact: item.exact === true, timeoutMs, pollMs };
|
|
33531
|
-
}
|
|
33532
|
-
fail(`Batch action ${index} has unsupported action ${JSON.stringify(item.action)}`);
|
|
33533
|
-
});
|
|
33534
|
-
}
|
|
33535
|
-
async function computerBrowserBatchCommand(actionsJson, options = {}) {
|
|
33536
|
-
const actions = parseBrowserBatchActions(actionsJson);
|
|
33537
|
-
const page = await selectChromePage(options);
|
|
33538
|
-
const { targetId, cached: cached2 } = readBrowserRefCache(page);
|
|
33539
|
-
const startedAt = Date.now();
|
|
33540
|
-
const completed = [];
|
|
33541
|
-
try {
|
|
33542
|
-
await withChromeSession(page.webSocketDebuggerUrl, async (session) => {
|
|
33543
|
-
const layout = await sendChromeSessionCommand(session, "Page.getLayoutMetrics", {});
|
|
33544
|
-
const viewportRect = browserViewportRect(layout);
|
|
33545
|
-
for (const [index, action] of actions.entries()) {
|
|
33546
|
-
const actionStartedAt = Date.now();
|
|
33547
|
-
if (action.action === "click") {
|
|
33548
|
-
const ref = parseBrowserRef(action.ref);
|
|
33549
|
-
const rect = await prepareBrowserRefInSession(session, ref, cached2, viewportRect);
|
|
33550
|
-
await dispatchChromeClickInSession(session, rect.centerX, rect.centerY, action.button, action.clicks);
|
|
33551
|
-
logRecordingAction({ type: "click" });
|
|
33552
|
-
completed.push({ index, action: action.action, ref: String(ref), elapsedMs: Date.now() - actionStartedAt });
|
|
33553
|
-
} else if (action.action === "fill") {
|
|
33554
|
-
const ref = parseBrowserRef(action.ref);
|
|
33555
|
-
await prepareBrowserRefInSession(session, ref, cached2, viewportRect);
|
|
33556
|
-
const result = await fillChromeNodeInSession(session, ref, action.value);
|
|
33557
|
-
logRecordingAction({ type: "type" });
|
|
33558
|
-
completed.push({ index, action: action.action, ref: String(ref), valueLength: result.valueLength, elapsedMs: Date.now() - actionStartedAt });
|
|
33559
|
-
} else if (action.action === "key") {
|
|
33560
|
-
await dispatchChromeKeyInSession(session, browserKeyDetails(action.combo));
|
|
33561
|
-
logRecordingAction({ type: "key" });
|
|
33562
|
-
completed.push({ index, action: action.action, elapsedMs: Date.now() - actionStartedAt });
|
|
33563
|
-
} else if (action.action === "type") {
|
|
33564
|
-
await sendChromeSessionCommand(session, "Input.insertText", { text: action.text });
|
|
33565
|
-
logRecordingAction({ type: "type" });
|
|
33566
|
-
completed.push({ index, action: action.action, length: action.text.length, elapsedMs: Date.now() - actionStartedAt });
|
|
33567
|
-
} else if (action.action === "scroll") {
|
|
33568
|
-
const ref = action.ref ? parseBrowserRef(action.ref) : null;
|
|
33569
|
-
const rect = ref ? await prepareBrowserRefInSession(session, ref, cached2, viewportRect) : viewportRect;
|
|
33570
|
-
const deltaX = action.direction === "left" ? -action.amount : action.direction === "right" ? action.amount : 0;
|
|
33571
|
-
const deltaY = action.direction === "up" ? -action.amount : action.direction === "down" ? action.amount : 0;
|
|
33572
|
-
await sendChromeSessionCommand(session, "Input.dispatchMouseEvent", {
|
|
33573
|
-
type: "mouseWheel",
|
|
33574
|
-
x: rect.centerX,
|
|
33575
|
-
y: rect.centerY,
|
|
33576
|
-
deltaX,
|
|
33577
|
-
deltaY
|
|
33578
|
-
});
|
|
33579
|
-
logRecordingAction({ type: "scroll" });
|
|
33580
|
-
completed.push({ index, action: action.action, direction: action.direction, amount: action.amount, elapsedMs: Date.now() - actionStartedAt });
|
|
33581
|
-
} else {
|
|
33582
|
-
const expression = buildBrowserWaitExpression(action.text, { mode: action.mode, exact: action.exact });
|
|
33583
|
-
let attempts = 0;
|
|
33584
|
-
let result = null;
|
|
33585
|
-
while (Date.now() - actionStartedAt <= action.timeoutMs) {
|
|
33586
|
-
attempts++;
|
|
33587
|
-
result = await evaluateChromeSession(session, expression);
|
|
33588
|
-
if (isRecord(result) && result.matched === true) break;
|
|
33589
|
-
await sleep2(action.pollMs);
|
|
33590
|
-
}
|
|
33591
|
-
if (!isRecord(result) || result.matched !== true) fail(`Batch wait ${index} timed out after ${Date.now() - actionStartedAt}ms`);
|
|
33592
|
-
completed.push({ index, action: action.action, attempts, elapsedMs: Date.now() - actionStartedAt });
|
|
33593
|
-
}
|
|
33594
|
-
}
|
|
33595
|
-
});
|
|
33596
|
-
} catch (error51) {
|
|
33597
|
-
const message = error51 instanceof Error ? error51.message : String(error51);
|
|
33598
|
-
console.log(JSON.stringify({ ok: false, targetId, elapsedMs: Date.now() - startedAt, completed, failedIndex: completed.length, error: message }, null, 2));
|
|
33599
|
-
throw error51;
|
|
33600
|
-
}
|
|
33601
|
-
console.log(JSON.stringify({ ok: true, targetId, elapsedMs: Date.now() - startedAt, completed }, null, 2));
|
|
33602
|
-
}
|
|
33603
|
-
async function computerClickCommand(xStr, yStr, options) {
|
|
33604
|
-
const dimensions = getDisplayDimensions();
|
|
33605
|
-
const x = parseScreenCoord(xStr, "x", dimensions.width);
|
|
33606
|
-
const y = parseScreenCoord(yStr, "y", dimensions.height);
|
|
33607
|
-
const button = options.button ?? "1";
|
|
33608
|
-
const args = ["mousemove", "--sync", String(x), String(y)];
|
|
33609
|
-
if (options.modifiers) {
|
|
33610
|
-
for (const mod of options.modifiers.split("+")) {
|
|
33611
|
-
args.push("keydown", mod);
|
|
33612
|
-
}
|
|
33613
|
-
}
|
|
33614
|
-
if (options.double) {
|
|
33615
|
-
args.push("click", "--repeat", "2", "--delay", "50", button);
|
|
33616
|
-
} else {
|
|
33617
|
-
args.push("click", button);
|
|
33618
|
-
}
|
|
33619
|
-
if (options.modifiers) {
|
|
33620
|
-
for (const mod of options.modifiers.split("+").reverse()) {
|
|
33621
|
-
args.push("keyup", mod);
|
|
33622
|
-
}
|
|
33623
|
-
}
|
|
33624
|
-
runDesktopInputCmd(args);
|
|
33625
|
-
logRecordingAction({ type: "click", x, y });
|
|
33626
|
-
console.log(`clicked ${button === "1" ? "left" : button === "2" ? "middle" : button === "3" ? "right" : `button ${button}`} at (${x},${y})${options.double ? " x2" : ""}`);
|
|
33627
|
-
}
|
|
33628
|
-
async function computerMoveCommand(xStr, yStr) {
|
|
33629
|
-
const dimensions = getDisplayDimensions();
|
|
33630
|
-
const x = parseScreenCoord(xStr, "x", dimensions.width);
|
|
33631
|
-
const y = parseScreenCoord(yStr, "y", dimensions.height);
|
|
33632
|
-
runDesktopInputCmd(["mousemove", "--sync", String(x), String(y)]);
|
|
33633
|
-
logRecordingAction({ type: "move", x, y });
|
|
33634
|
-
console.log(`moved to (${x},${y})`);
|
|
33635
|
-
}
|
|
33636
|
-
async function computerTypeCommand(text, options) {
|
|
33637
|
-
const delay = options.delay ? parseCoord(options.delay, "--delay") : 12;
|
|
33638
|
-
runDesktopInputCmd(["type", "--delay", String(delay), "--", text]);
|
|
33639
|
-
const position = recordingMousePosition();
|
|
33640
|
-
logRecordingAction({ type: "type", ...position ?? {} });
|
|
33641
|
-
console.log(`typed ${text.length} char${text.length === 1 ? "" : "s"}`);
|
|
33642
|
-
}
|
|
33643
|
-
async function computerKeyCommand(combo) {
|
|
33644
|
-
runDesktopInputCmd(["key", "--", combo]);
|
|
33645
|
-
const position = recordingMousePosition();
|
|
33646
|
-
logRecordingAction({ type: "key", ...position ?? {} });
|
|
33647
|
-
console.log(`pressed ${combo}`);
|
|
33648
|
-
}
|
|
33649
|
-
async function computerScrollCommand(direction, options) {
|
|
33650
|
-
const dir = direction.toLowerCase();
|
|
33651
|
-
const buttonMap = { up: "4", down: "5", left: "6", right: "7" };
|
|
33652
|
-
const button = buttonMap[dir];
|
|
33653
|
-
if (!button) fail(`direction must be one of up|down|left|right (got "${direction}")`);
|
|
33654
|
-
const amount = options.amount ? parseCoord(options.amount, "--amount") : 3;
|
|
33655
|
-
const args = [];
|
|
33656
|
-
let hoverPosition = null;
|
|
33657
|
-
if (options.x && options.y) {
|
|
33658
|
-
const dimensions = getDisplayDimensions();
|
|
33659
|
-
hoverPosition = {
|
|
33660
|
-
x: parseScreenCoord(options.x, "--x", dimensions.width),
|
|
33661
|
-
y: parseScreenCoord(options.y, "--y", dimensions.height)
|
|
33662
|
-
};
|
|
33663
|
-
args.push(
|
|
33664
|
-
"mousemove",
|
|
33665
|
-
"--sync",
|
|
33666
|
-
String(hoverPosition.x),
|
|
33667
|
-
String(hoverPosition.y)
|
|
33668
|
-
);
|
|
33669
|
-
}
|
|
33670
|
-
args.push("click", "--repeat", String(amount), "--delay", "30", button);
|
|
33671
|
-
runDesktopInputCmd(args);
|
|
33672
|
-
const position = hoverPosition ?? recordingMousePosition();
|
|
33673
|
-
logRecordingAction({ type: "scroll", ...position ?? {} });
|
|
33674
|
-
console.log(`scrolled ${dir} x${amount}`);
|
|
33675
|
-
}
|
|
33676
|
-
async function computerDragCommand(fx, fy, tx, ty) {
|
|
33677
|
-
const dimensions = getDisplayDimensions();
|
|
33678
|
-
const fromX = parseScreenCoord(fx, "fromX", dimensions.width);
|
|
33679
|
-
const fromY = parseScreenCoord(fy, "fromY", dimensions.height);
|
|
33680
|
-
const toX = parseScreenCoord(tx, "toX", dimensions.width);
|
|
33681
|
-
const toY = parseScreenCoord(ty, "toY", dimensions.height);
|
|
33682
|
-
const distance = Math.hypot(toX - fromX, toY - fromY);
|
|
33683
|
-
const steps = clamp(Math.ceil(distance / 70), 8, 24);
|
|
33684
|
-
const args = [
|
|
33685
|
-
"mousemove",
|
|
33686
|
-
"--sync",
|
|
33687
|
-
String(fromX),
|
|
33688
|
-
String(fromY),
|
|
33689
|
-
"mousedown",
|
|
33690
|
-
"1"
|
|
33691
|
-
];
|
|
33692
|
-
for (let i = 1; i <= steps; i++) {
|
|
33693
|
-
const x = Math.round(fromX + (toX - fromX) * i / steps);
|
|
33694
|
-
const y = Math.round(fromY + (toY - fromY) * i / steps);
|
|
33695
|
-
args.push("mousemove", "--sync", String(x), String(y), "sleep", "0.018");
|
|
33696
|
-
}
|
|
33697
|
-
args.push("mouseup", "1");
|
|
33698
|
-
runDesktopInputCmd(args);
|
|
33699
|
-
logRecordingAction({ type: "drag", x: fromX, y: fromY, toX, toY });
|
|
33700
|
-
console.log(`dragged (${fromX},${fromY}) -> (${toX},${toY})`);
|
|
33701
|
-
}
|
|
33702
|
-
var APP_ALIASES = {
|
|
33703
|
-
chrome: [CHROME_WRAPPER],
|
|
33704
|
-
chromium: ["chromium", "--no-first-run", "--no-default-browser-check"],
|
|
33705
|
-
firefox: ["firefox"],
|
|
33706
|
-
terminal: ["xfce4-terminal"],
|
|
33707
|
-
xterm: ["xterm"],
|
|
33708
|
-
notepad: ["mousepad"],
|
|
33709
|
-
editor: ["mousepad"],
|
|
33710
|
-
files: ["thunar"],
|
|
33711
|
-
filemanager: ["thunar"]
|
|
33712
|
-
};
|
|
33713
|
-
var LEGACY_CHROME_ALIAS = [
|
|
33714
|
-
"google-chrome",
|
|
33715
|
-
"--no-first-run",
|
|
33716
|
-
"--no-default-browser-check",
|
|
33717
|
-
"--start-maximized",
|
|
33718
|
-
"--user-data-dir=/tmp/replicas-computer/chrome-profile"
|
|
33719
|
-
];
|
|
33720
|
-
async function computerLaunchCommand(app, args) {
|
|
33721
|
-
ensureServicesRunning();
|
|
33722
|
-
const baseArgs = app === "chrome" && !existsSync3(CHROME_WRAPPER) ? LEGACY_CHROME_ALIAS : APP_ALIASES[app] ?? [app];
|
|
33723
|
-
const bin = baseArgs[0];
|
|
33724
|
-
const fullArgs = [...baseArgs.slice(1), ...args];
|
|
33725
|
-
const expectsNewChromePage = args.some((arg) => !arg.startsWith("-"));
|
|
33726
|
-
const existingChromePageIds = /* @__PURE__ */ new Set();
|
|
33727
|
-
if (app === "chrome") {
|
|
33728
|
-
try {
|
|
33729
|
-
for (const page of await getChromePages()) {
|
|
33730
|
-
if (page.id) existingChromePageIds.add(page.id);
|
|
33731
|
-
}
|
|
33732
|
-
} catch {
|
|
33733
|
-
}
|
|
33734
|
-
}
|
|
33735
|
-
const child = spawn5(bin, fullArgs, {
|
|
33736
|
-
env: withDisplay(),
|
|
33737
|
-
detached: true,
|
|
33738
|
-
stdio: "ignore"
|
|
33739
|
-
});
|
|
33740
|
-
child.unref();
|
|
33741
|
-
if (app === "chrome") {
|
|
33742
|
-
const startedAt = Date.now();
|
|
33743
|
-
let pageCount = 0;
|
|
33744
|
-
let readyPage = null;
|
|
33745
|
-
while (Date.now() - startedAt < 15e3) {
|
|
33746
|
-
let pages = [];
|
|
33747
|
-
try {
|
|
33748
|
-
pages = await getChromePages();
|
|
33749
|
-
} catch {
|
|
33750
|
-
}
|
|
33751
|
-
pageCount = pages.length;
|
|
33752
|
-
const candidates = expectsNewChromePage && existingChromePageIds.size > 0 ? pages.filter((page) => page.id && !existingChromePageIds.has(page.id)) : pages;
|
|
33753
|
-
for (const page of candidates) {
|
|
33754
|
-
if (!page.webSocketDebuggerUrl || !page.url || page.url === "about:blank") continue;
|
|
33755
|
-
const readyState = await evaluateChromeTarget(page.webSocketDebuggerUrl, "document.readyState");
|
|
33756
|
-
if (readyState === "interactive" || readyState === "complete") {
|
|
33757
|
-
readyPage = page;
|
|
33758
|
-
break;
|
|
33759
|
-
}
|
|
33760
|
-
}
|
|
33761
|
-
if (readyPage) break;
|
|
33762
|
-
try {
|
|
33763
|
-
if (child.pid) process.kill(child.pid, 0);
|
|
33764
|
-
} catch {
|
|
33765
|
-
fail(`Chrome exited before its desktop control channel became ready.`);
|
|
33766
|
-
}
|
|
33767
|
-
await sleep2(100);
|
|
33768
|
-
}
|
|
33769
|
-
if (!readyPage?.webSocketDebuggerUrl) fail(`Chrome launched but its requested page was not controllable after 15 seconds.`);
|
|
33770
|
-
await sendChromeCommand(readyPage.webSocketDebuggerUrl, "Page.bringToFront", {});
|
|
33771
|
-
console.log(`launched ${bin} (pid ${child.pid}, ready in ${Date.now() - startedAt}ms, page ${readyPage.id}, ${pageCount} open)`);
|
|
33772
|
-
return;
|
|
33773
|
-
}
|
|
33774
|
-
console.log(`launched ${bin} (pid ${child.pid})`);
|
|
33775
|
-
}
|
|
33776
|
-
|
|
33777
30907
|
// src/commands/interactive.ts
|
|
33778
|
-
import
|
|
30908
|
+
import chalk21 from "chalk";
|
|
33779
30909
|
|
|
33780
30910
|
// src/interactive/index.tsx
|
|
33781
30911
|
import { createCliRenderer } from "@opentui/core";
|
|
@@ -33849,7 +30979,7 @@ function useReconnectingSseStream(options) {
|
|
|
33849
30979
|
while (!cancelled) {
|
|
33850
30980
|
const stop = await connect();
|
|
33851
30981
|
if (cancelled || stop) break;
|
|
33852
|
-
await new Promise((
|
|
30982
|
+
await new Promise((resolve2) => setTimeout(resolve2, reconnectDelayMs));
|
|
33853
30983
|
}
|
|
33854
30984
|
};
|
|
33855
30985
|
run().catch(() => setConnected(false));
|
|
@@ -37084,13 +34214,13 @@ async function interactiveCommand() {
|
|
|
37084
34214
|
'No organization selected. Please run "replicas org switch" to select an organization.'
|
|
37085
34215
|
);
|
|
37086
34216
|
}
|
|
37087
|
-
console.log(
|
|
34217
|
+
console.log(chalk21.gray("Starting interactive mode..."));
|
|
37088
34218
|
await launchInteractive();
|
|
37089
34219
|
}
|
|
37090
34220
|
|
|
37091
34221
|
// src/commands/environment.ts
|
|
37092
34222
|
import fs5 from "fs";
|
|
37093
|
-
import
|
|
34223
|
+
import chalk22 from "chalk";
|
|
37094
34224
|
import prompts6 from "prompts";
|
|
37095
34225
|
var UUID_RE = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/;
|
|
37096
34226
|
function maskValue(value) {
|
|
@@ -37103,38 +34233,38 @@ async function resolveEnvironmentId(input) {
|
|
|
37103
34233
|
const response = await orgAuthenticatedFetch("/v1/environments");
|
|
37104
34234
|
const resolved = resolveByNameOrId(input, response.environments);
|
|
37105
34235
|
if (!resolved) {
|
|
37106
|
-
console.log(
|
|
34236
|
+
console.log(chalk22.red(`Environment not found: ${input}`));
|
|
37107
34237
|
const available = response.environments.map((e) => e.name).join(", ");
|
|
37108
|
-
console.log(
|
|
34238
|
+
console.log(chalk22.gray(`Available: ${available || "(none)"}`));
|
|
37109
34239
|
process.exit(1);
|
|
37110
34240
|
}
|
|
37111
34241
|
return resolved.id;
|
|
37112
34242
|
}
|
|
37113
34243
|
function printEnvironment(env) {
|
|
37114
|
-
console.log(
|
|
37115
|
-
console.log(
|
|
34244
|
+
console.log(chalk22.white(` ${env.name}${env.is_global ? chalk22.gray(" (global)") : ""}`));
|
|
34245
|
+
console.log(chalk22.gray(` ID: ${env.id}`));
|
|
37116
34246
|
if (env.description) {
|
|
37117
|
-
console.log(
|
|
34247
|
+
console.log(chalk22.gray(` Description: ${env.description}`));
|
|
37118
34248
|
}
|
|
37119
34249
|
if (env.repository_id) {
|
|
37120
|
-
console.log(
|
|
34250
|
+
console.log(chalk22.gray(` Repository: ${env.repository_id}`));
|
|
37121
34251
|
} else if (env.repository_set_id) {
|
|
37122
|
-
console.log(
|
|
34252
|
+
console.log(chalk22.gray(` Repository Set: ${env.repository_set_id}`));
|
|
37123
34253
|
}
|
|
37124
34254
|
if (env.variable_count !== void 0) {
|
|
37125
|
-
console.log(
|
|
34255
|
+
console.log(chalk22.gray(` Variables: ${env.variable_count}, Files: ${env.file_count ?? 0}, Skills: ${env.skill_count ?? 0}, MCPs: ${env.mcp_count ?? 0}`));
|
|
37126
34256
|
}
|
|
37127
|
-
console.log(
|
|
34257
|
+
console.log(chalk22.gray(` Updated: ${formatDate2(env.updated_at)}`));
|
|
37128
34258
|
console.log();
|
|
37129
34259
|
}
|
|
37130
34260
|
async function environmentListCommand() {
|
|
37131
34261
|
ensureOrgApiAuthenticated();
|
|
37132
34262
|
const response = await orgAuthenticatedFetch("/v1/environments");
|
|
37133
34263
|
if (response.environments.length === 0) {
|
|
37134
|
-
console.log(
|
|
34264
|
+
console.log(chalk22.yellow("\nNo environments found.\n"));
|
|
37135
34265
|
return;
|
|
37136
34266
|
}
|
|
37137
|
-
console.log(
|
|
34267
|
+
console.log(chalk22.green(`
|
|
37138
34268
|
Environments (${response.environments.length}):
|
|
37139
34269
|
`));
|
|
37140
34270
|
for (const env of response.environments) {
|
|
@@ -37145,7 +34275,7 @@ async function environmentGetCommand(idOrName) {
|
|
|
37145
34275
|
ensureOrgApiAuthenticated();
|
|
37146
34276
|
const id = await resolveEnvironmentId(idOrName);
|
|
37147
34277
|
const response = await orgAuthenticatedFetch(`/v1/environments/${id}`);
|
|
37148
|
-
console.log(
|
|
34278
|
+
console.log(chalk22.green(`
|
|
37149
34279
|
Environment: ${response.environment.name}
|
|
37150
34280
|
`));
|
|
37151
34281
|
printEnvironment(response.environment);
|
|
@@ -37161,7 +34291,7 @@ async function environmentCreateCommand(name, options) {
|
|
|
37161
34291
|
validate: (v) => v.trim() ? true : "Name is required"
|
|
37162
34292
|
});
|
|
37163
34293
|
if (!r.name) {
|
|
37164
|
-
console.log(
|
|
34294
|
+
console.log(chalk22.yellow("\nCancelled."));
|
|
37165
34295
|
return;
|
|
37166
34296
|
}
|
|
37167
34297
|
envName = r.name;
|
|
@@ -37174,8 +34304,8 @@ async function environmentCreateCommand(name, options) {
|
|
|
37174
34304
|
const repos2 = await orgAuthenticatedFetch("/v1/repositories");
|
|
37175
34305
|
const repo = repos2.repositories.find((r) => r.name === options.repository);
|
|
37176
34306
|
if (!repo) {
|
|
37177
|
-
console.log(
|
|
37178
|
-
console.log(
|
|
34307
|
+
console.log(chalk22.red(`Repository not found: ${options.repository}`));
|
|
34308
|
+
console.log(chalk22.gray(`Available: ${repos2.repositories.map((r) => r.name).join(", ")}`));
|
|
37179
34309
|
process.exit(1);
|
|
37180
34310
|
}
|
|
37181
34311
|
repositoryId = repo.id;
|
|
@@ -37205,9 +34335,9 @@ async function environmentCreateCommand(name, options) {
|
|
|
37205
34335
|
method: "POST",
|
|
37206
34336
|
body
|
|
37207
34337
|
});
|
|
37208
|
-
console.log(
|
|
34338
|
+
console.log(chalk22.green(`
|
|
37209
34339
|
Created environment: ${response.environment.name}`));
|
|
37210
|
-
console.log(
|
|
34340
|
+
console.log(chalk22.gray(` ID: ${response.environment.id}
|
|
37211
34341
|
`));
|
|
37212
34342
|
}
|
|
37213
34343
|
async function environmentEditCommand(idOrName, options) {
|
|
@@ -37226,21 +34356,21 @@ async function environmentEditCommand(idOrName, options) {
|
|
|
37226
34356
|
const repos2 = await orgAuthenticatedFetch("/v1/repositories");
|
|
37227
34357
|
const repo = repos2.repositories.find((r) => r.name === options.repository);
|
|
37228
34358
|
if (!repo) {
|
|
37229
|
-
console.log(
|
|
34359
|
+
console.log(chalk22.red(`Repository not found: ${options.repository}`));
|
|
37230
34360
|
process.exit(1);
|
|
37231
34361
|
}
|
|
37232
34362
|
body.repository_id = repo.id;
|
|
37233
34363
|
}
|
|
37234
34364
|
}
|
|
37235
34365
|
if (Object.keys(body).length === 0) {
|
|
37236
|
-
console.log(
|
|
34366
|
+
console.log(chalk22.yellow("\nNo changes specified. Pass --name, --description, --repository, or --system-prompt."));
|
|
37237
34367
|
return;
|
|
37238
34368
|
}
|
|
37239
34369
|
const response = await orgAuthenticatedFetch(`/v1/environments/${id}`, {
|
|
37240
34370
|
method: "PATCH",
|
|
37241
34371
|
body
|
|
37242
34372
|
});
|
|
37243
|
-
console.log(
|
|
34373
|
+
console.log(chalk22.green(`
|
|
37244
34374
|
Updated environment: ${response.environment.name}
|
|
37245
34375
|
`));
|
|
37246
34376
|
}
|
|
@@ -37255,20 +34385,20 @@ async function environmentDeleteCommand(idOrName, options) {
|
|
|
37255
34385
|
initial: false
|
|
37256
34386
|
});
|
|
37257
34387
|
if (!r.confirm) {
|
|
37258
|
-
console.log(
|
|
34388
|
+
console.log(chalk22.yellow("\nCancelled."));
|
|
37259
34389
|
return;
|
|
37260
34390
|
}
|
|
37261
34391
|
}
|
|
37262
34392
|
await orgAuthenticatedFetch(`/v1/environments/${id}`, { method: "DELETE" });
|
|
37263
|
-
console.log(
|
|
34393
|
+
console.log(chalk22.green(`
|
|
37264
34394
|
Deleted environment ${idOrName}.
|
|
37265
34395
|
`));
|
|
37266
34396
|
}
|
|
37267
34397
|
function printVariable(v, reveal) {
|
|
37268
|
-
console.log(
|
|
37269
|
-
console.log(
|
|
37270
|
-
console.log(
|
|
37271
|
-
console.log(
|
|
34398
|
+
console.log(chalk22.white(` ${v.key}`));
|
|
34399
|
+
console.log(chalk22.gray(` ID: ${v.id}`));
|
|
34400
|
+
console.log(chalk22.gray(` Value: ${reveal ? v.value : maskValue(v.value)}`));
|
|
34401
|
+
console.log(chalk22.gray(` Updated: ${formatDate2(v.updated_at)}`));
|
|
37272
34402
|
console.log();
|
|
37273
34403
|
}
|
|
37274
34404
|
async function envVarsListCommand(envIdOrName, options) {
|
|
@@ -37278,14 +34408,14 @@ async function envVarsListCommand(envIdOrName, options) {
|
|
|
37278
34408
|
`/v1/environments/${id}/variables`
|
|
37279
34409
|
);
|
|
37280
34410
|
if (response.environment_variables.length === 0) {
|
|
37281
|
-
console.log(
|
|
34411
|
+
console.log(chalk22.yellow("\nNo variables.\n"));
|
|
37282
34412
|
return;
|
|
37283
34413
|
}
|
|
37284
|
-
console.log(
|
|
34414
|
+
console.log(chalk22.green(`
|
|
37285
34415
|
Variables (${response.environment_variables.length}):
|
|
37286
34416
|
`));
|
|
37287
34417
|
if (!options.reveal) {
|
|
37288
|
-
console.log(
|
|
34418
|
+
console.log(chalk22.gray(" Values are masked. Pass --reveal to show full values.\n"));
|
|
37289
34419
|
}
|
|
37290
34420
|
for (const v of response.environment_variables) printVariable(v, !!options.reveal);
|
|
37291
34421
|
}
|
|
@@ -37302,7 +34432,7 @@ async function envVarsSetCommand(envIdOrName, key, value) {
|
|
|
37302
34432
|
`/v1/environments/${id}/variables/${match.id}`,
|
|
37303
34433
|
{ method: "PATCH", body: body2 }
|
|
37304
34434
|
);
|
|
37305
|
-
console.log(
|
|
34435
|
+
console.log(chalk22.green(`
|
|
37306
34436
|
Updated variable ${response2.environment_variable.key}.
|
|
37307
34437
|
`));
|
|
37308
34438
|
return;
|
|
@@ -37316,7 +34446,7 @@ Updated variable ${response2.environment_variable.key}.
|
|
|
37316
34446
|
`/v1/environments/${id}/variables`,
|
|
37317
34447
|
{ method: "POST", body }
|
|
37318
34448
|
);
|
|
37319
|
-
console.log(
|
|
34449
|
+
console.log(chalk22.green(`
|
|
37320
34450
|
Created variable ${response.environment_variable.key}.
|
|
37321
34451
|
`));
|
|
37322
34452
|
}
|
|
@@ -37330,7 +34460,7 @@ async function envVarsDeleteCommand(envIdOrName, keyOrId, options) {
|
|
|
37330
34460
|
);
|
|
37331
34461
|
const match = existing.environment_variables.find((v) => v.key === keyOrId);
|
|
37332
34462
|
if (!match) {
|
|
37333
|
-
console.log(
|
|
34463
|
+
console.log(chalk22.red(`Variable not found: ${keyOrId}`));
|
|
37334
34464
|
process.exit(1);
|
|
37335
34465
|
}
|
|
37336
34466
|
variableId = match.id;
|
|
@@ -37343,23 +34473,23 @@ async function envVarsDeleteCommand(envIdOrName, keyOrId, options) {
|
|
|
37343
34473
|
initial: false
|
|
37344
34474
|
});
|
|
37345
34475
|
if (!r.confirm) {
|
|
37346
|
-
console.log(
|
|
34476
|
+
console.log(chalk22.yellow("\nCancelled."));
|
|
37347
34477
|
return;
|
|
37348
34478
|
}
|
|
37349
34479
|
}
|
|
37350
34480
|
await orgAuthenticatedFetch(`/v1/environments/${id}/variables/${variableId}`, {
|
|
37351
34481
|
method: "DELETE"
|
|
37352
34482
|
});
|
|
37353
|
-
console.log(
|
|
34483
|
+
console.log(chalk22.green(`
|
|
37354
34484
|
Deleted variable ${keyOrId}.
|
|
37355
34485
|
`));
|
|
37356
34486
|
}
|
|
37357
34487
|
function printFile(f) {
|
|
37358
|
-
console.log(
|
|
37359
|
-
console.log(
|
|
37360
|
-
console.log(
|
|
37361
|
-
console.log(
|
|
37362
|
-
console.log(
|
|
34488
|
+
console.log(chalk22.white(` ${f.path}`));
|
|
34489
|
+
console.log(chalk22.gray(` ID: ${f.id}`));
|
|
34490
|
+
console.log(chalk22.gray(` Name: ${f.name}`));
|
|
34491
|
+
console.log(chalk22.gray(` Size: ${f.content.length} bytes`));
|
|
34492
|
+
console.log(chalk22.gray(` Updated: ${formatDate2(f.updated_at)}`));
|
|
37363
34493
|
console.log();
|
|
37364
34494
|
}
|
|
37365
34495
|
async function envFilesListCommand(envIdOrName) {
|
|
@@ -37369,10 +34499,10 @@ async function envFilesListCommand(envIdOrName) {
|
|
|
37369
34499
|
`/v1/environments/${id}/files`
|
|
37370
34500
|
);
|
|
37371
34501
|
if (response.environment_files.length === 0) {
|
|
37372
|
-
console.log(
|
|
34502
|
+
console.log(chalk22.yellow("\nNo files.\n"));
|
|
37373
34503
|
return;
|
|
37374
34504
|
}
|
|
37375
|
-
console.log(
|
|
34505
|
+
console.log(chalk22.green(`
|
|
37376
34506
|
Files (${response.environment_files.length}):
|
|
37377
34507
|
`));
|
|
37378
34508
|
for (const f of response.environment_files) printFile(f);
|
|
@@ -37403,7 +34533,7 @@ async function envFilesSetCommand(envIdOrName, destinationPath, options) {
|
|
|
37403
34533
|
`/v1/environments/${id}/files/${match.id}`,
|
|
37404
34534
|
{ method: "PATCH", body: body2 }
|
|
37405
34535
|
);
|
|
37406
|
-
console.log(
|
|
34536
|
+
console.log(chalk22.green(`
|
|
37407
34537
|
Updated file ${response2.environment_file.path}.
|
|
37408
34538
|
`));
|
|
37409
34539
|
return;
|
|
@@ -37418,7 +34548,7 @@ Updated file ${response2.environment_file.path}.
|
|
|
37418
34548
|
`/v1/environments/${id}/files`,
|
|
37419
34549
|
{ method: "POST", body }
|
|
37420
34550
|
);
|
|
37421
|
-
console.log(
|
|
34551
|
+
console.log(chalk22.green(`
|
|
37422
34552
|
Created file ${response.environment_file.path}.
|
|
37423
34553
|
`));
|
|
37424
34554
|
}
|
|
@@ -37432,7 +34562,7 @@ async function envFilesDeleteCommand(envIdOrName, pathOrId, options) {
|
|
|
37432
34562
|
);
|
|
37433
34563
|
const match = existing.environment_files.find((f) => f.path === pathOrId);
|
|
37434
34564
|
if (!match) {
|
|
37435
|
-
console.log(
|
|
34565
|
+
console.log(chalk22.red(`File not found: ${pathOrId}`));
|
|
37436
34566
|
process.exit(1);
|
|
37437
34567
|
}
|
|
37438
34568
|
fileId = match.id;
|
|
@@ -37445,14 +34575,14 @@ async function envFilesDeleteCommand(envIdOrName, pathOrId, options) {
|
|
|
37445
34575
|
initial: false
|
|
37446
34576
|
});
|
|
37447
34577
|
if (!r.confirm) {
|
|
37448
|
-
console.log(
|
|
34578
|
+
console.log(chalk22.yellow("\nCancelled."));
|
|
37449
34579
|
return;
|
|
37450
34580
|
}
|
|
37451
34581
|
}
|
|
37452
34582
|
await orgAuthenticatedFetch(`/v1/environments/${id}/files/${fileId}`, {
|
|
37453
34583
|
method: "DELETE"
|
|
37454
34584
|
});
|
|
37455
|
-
console.log(
|
|
34585
|
+
console.log(chalk22.green(`
|
|
37456
34586
|
Deleted file ${pathOrId}.
|
|
37457
34587
|
`));
|
|
37458
34588
|
}
|
|
@@ -37463,15 +34593,15 @@ async function envStartHookGetCommand(envIdOrName) {
|
|
|
37463
34593
|
`/v1/environments/${id}/start-hooks`
|
|
37464
34594
|
);
|
|
37465
34595
|
if (!response.start_hook) {
|
|
37466
|
-
console.log(
|
|
34596
|
+
console.log(chalk22.yellow("\nNo start hook configured.\n"));
|
|
37467
34597
|
return;
|
|
37468
34598
|
}
|
|
37469
34599
|
const hook = response.start_hook;
|
|
37470
|
-
console.log(
|
|
34600
|
+
console.log(chalk22.green(`
|
|
37471
34601
|
Start hook (v${hook.version}, ${hook.is_active ? "active" : "inactive"}):
|
|
37472
34602
|
`));
|
|
37473
|
-
console.log(
|
|
37474
|
-
console.log(
|
|
34603
|
+
console.log(chalk22.gray(` ID: ${hook.id}`));
|
|
34604
|
+
console.log(chalk22.gray(` Created: ${formatDate2(hook.created_at)}
|
|
37475
34605
|
`));
|
|
37476
34606
|
console.log(hook.content);
|
|
37477
34607
|
console.log();
|
|
@@ -37486,10 +34616,10 @@ async function envStartHookSaveCommand(envIdOrName, options) {
|
|
|
37486
34616
|
{ method: "POST", body }
|
|
37487
34617
|
);
|
|
37488
34618
|
if (!response.start_hook) {
|
|
37489
|
-
console.log(
|
|
34619
|
+
console.log(chalk22.green("\nCleared start hook.\n"));
|
|
37490
34620
|
return;
|
|
37491
34621
|
}
|
|
37492
|
-
console.log(
|
|
34622
|
+
console.log(chalk22.green(`
|
|
37493
34623
|
Saved start hook v${response.start_hook.version}.
|
|
37494
34624
|
`));
|
|
37495
34625
|
}
|
|
@@ -37506,7 +34636,7 @@ async function envStartHookTestCommand(envIdOrName, options) {
|
|
|
37506
34636
|
body: { content },
|
|
37507
34637
|
onEvent: (event) => {
|
|
37508
34638
|
if (event.type === "progress" && event.message) {
|
|
37509
|
-
console.log(
|
|
34639
|
+
console.log(chalk22.gray(event.message));
|
|
37510
34640
|
} else if (event.type === "output" && event.output) {
|
|
37511
34641
|
process.stdout.write(event.output);
|
|
37512
34642
|
} else if (event.type === "complete") {
|
|
@@ -37519,21 +34649,21 @@ async function envStartHookTestCommand(envIdOrName, options) {
|
|
|
37519
34649
|
}
|
|
37520
34650
|
);
|
|
37521
34651
|
if (errorMessage) {
|
|
37522
|
-
console.log(
|
|
34652
|
+
console.log(chalk22.red(`
|
|
37523
34653
|
${errorMessage}
|
|
37524
34654
|
`));
|
|
37525
34655
|
process.exit(1);
|
|
37526
34656
|
}
|
|
37527
34657
|
if (timedOut) {
|
|
37528
|
-
console.log(
|
|
34658
|
+
console.log(chalk22.yellow("\nStart hook timed out.\n"));
|
|
37529
34659
|
process.exit(1);
|
|
37530
34660
|
}
|
|
37531
34661
|
if (exitCode === 0) {
|
|
37532
|
-
console.log(
|
|
34662
|
+
console.log(chalk22.green(`
|
|
37533
34663
|
Start hook passed (exit code ${exitCode}).
|
|
37534
34664
|
`));
|
|
37535
34665
|
} else {
|
|
37536
|
-
console.log(
|
|
34666
|
+
console.log(chalk22.red(`
|
|
37537
34667
|
Start hook failed (exit code ${exitCode ?? "unknown"}).
|
|
37538
34668
|
`));
|
|
37539
34669
|
process.exit(1);
|
|
@@ -37546,24 +34676,24 @@ async function envStartHookRepositoryHooksCommand(envIdOrName) {
|
|
|
37546
34676
|
`/v1/environments/${id}/start-hooks/repository-hooks`
|
|
37547
34677
|
);
|
|
37548
34678
|
if (response.repositories.length === 0) {
|
|
37549
|
-
console.log(
|
|
34679
|
+
console.log(chalk22.yellow("\nNo repositories bound to this environment.\n"));
|
|
37550
34680
|
return;
|
|
37551
34681
|
}
|
|
37552
|
-
console.log(
|
|
34682
|
+
console.log(chalk22.green(`
|
|
37553
34683
|
Repository start hooks (${response.repositories.length}):
|
|
37554
34684
|
`));
|
|
37555
34685
|
for (const repo of response.repositories) {
|
|
37556
|
-
console.log(
|
|
34686
|
+
console.log(chalk22.white(` ${repo.repository_name} @${repo.default_branch}`));
|
|
37557
34687
|
if (repo.error) {
|
|
37558
|
-
console.log(
|
|
34688
|
+
console.log(chalk22.red(` Error: ${repo.error}`));
|
|
37559
34689
|
} else if (repo.start_hook) {
|
|
37560
|
-
console.log(
|
|
37561
|
-
console.log(
|
|
34690
|
+
console.log(chalk22.gray(` Source: ${repo.filename ?? "(unknown)"}`));
|
|
34691
|
+
console.log(chalk22.gray(` Commands (${repo.start_hook.commands.length}):`));
|
|
37562
34692
|
for (const cmd of repo.start_hook.commands) {
|
|
37563
|
-
console.log(
|
|
34693
|
+
console.log(chalk22.gray(` ${cmd}`));
|
|
37564
34694
|
}
|
|
37565
34695
|
} else {
|
|
37566
|
-
console.log(
|
|
34696
|
+
console.log(chalk22.gray(` No startHook defined.`));
|
|
37567
34697
|
}
|
|
37568
34698
|
console.log();
|
|
37569
34699
|
}
|
|
@@ -37585,7 +34715,7 @@ function registerSlackCommands(parent) {
|
|
|
37585
34715
|
await slackThreadAttachCommand(options);
|
|
37586
34716
|
} catch (error51) {
|
|
37587
34717
|
if (error51 instanceof Error) {
|
|
37588
|
-
console.error(
|
|
34718
|
+
console.error(chalk23.red(`
|
|
37589
34719
|
\u2717 ${error51.message}
|
|
37590
34720
|
`));
|
|
37591
34721
|
}
|
|
@@ -37597,7 +34727,7 @@ function registerSlackCommands(parent) {
|
|
|
37597
34727
|
await slackThreadSwitchCommand(workspace, options);
|
|
37598
34728
|
} catch (error51) {
|
|
37599
34729
|
if (error51 instanceof Error) {
|
|
37600
|
-
console.error(
|
|
34730
|
+
console.error(chalk23.red(`
|
|
37601
34731
|
\u2717 ${error51.message}
|
|
37602
34732
|
`));
|
|
37603
34733
|
}
|
|
@@ -37611,7 +34741,7 @@ program.command("login").description("Authenticate with your Replicas account").
|
|
|
37611
34741
|
await loginCommand();
|
|
37612
34742
|
} catch (error51) {
|
|
37613
34743
|
if (error51 instanceof Error) {
|
|
37614
|
-
console.error(
|
|
34744
|
+
console.error(chalk23.red(`
|
|
37615
34745
|
\u2717 ${error51.message}
|
|
37616
34746
|
`));
|
|
37617
34747
|
}
|
|
@@ -37623,7 +34753,7 @@ program.command("init").description("Create a replicas.json or replicas.yaml con
|
|
|
37623
34753
|
initCommand(options);
|
|
37624
34754
|
} catch (error51) {
|
|
37625
34755
|
if (error51 instanceof Error) {
|
|
37626
|
-
console.error(
|
|
34756
|
+
console.error(chalk23.red(`
|
|
37627
34757
|
\u2717 ${error51.message}
|
|
37628
34758
|
`));
|
|
37629
34759
|
}
|
|
@@ -37635,7 +34765,7 @@ program.command("logout").description("Clear stored credentials").action(() => {
|
|
|
37635
34765
|
logoutCommand();
|
|
37636
34766
|
} catch (error51) {
|
|
37637
34767
|
if (error51 instanceof Error) {
|
|
37638
|
-
console.error(
|
|
34768
|
+
console.error(chalk23.red(`
|
|
37639
34769
|
\u2717 ${error51.message}
|
|
37640
34770
|
`));
|
|
37641
34771
|
}
|
|
@@ -37647,7 +34777,7 @@ program.command("whoami").description("Display current authenticated user").acti
|
|
|
37647
34777
|
await whoamiCommand();
|
|
37648
34778
|
} catch (error51) {
|
|
37649
34779
|
if (error51 instanceof Error) {
|
|
37650
|
-
console.error(
|
|
34780
|
+
console.error(chalk23.red(`
|
|
37651
34781
|
\u2717 ${error51.message}
|
|
37652
34782
|
`));
|
|
37653
34783
|
}
|
|
@@ -37659,7 +34789,7 @@ program.command("codex-auth").description("Connect your Codex credentials to Rep
|
|
|
37659
34789
|
await codexAuthCommand(options);
|
|
37660
34790
|
} catch (error51) {
|
|
37661
34791
|
if (error51 instanceof Error) {
|
|
37662
|
-
console.error(
|
|
34792
|
+
console.error(chalk23.red(`
|
|
37663
34793
|
\u2717 ${error51.message}
|
|
37664
34794
|
`));
|
|
37665
34795
|
}
|
|
@@ -37671,7 +34801,7 @@ program.command("claude-auth").description("Connect your Claude Code credentials
|
|
|
37671
34801
|
await claudeAuthCommand(options);
|
|
37672
34802
|
} catch (error51) {
|
|
37673
34803
|
if (error51 instanceof Error) {
|
|
37674
|
-
console.error(
|
|
34804
|
+
console.error(chalk23.red(`
|
|
37675
34805
|
\u2717 ${error51.message}
|
|
37676
34806
|
`));
|
|
37677
34807
|
}
|
|
@@ -37684,7 +34814,7 @@ org.command("switch").description("Switch to a different organization").action(a
|
|
|
37684
34814
|
await orgSwitchCommand();
|
|
37685
34815
|
} catch (error51) {
|
|
37686
34816
|
if (error51 instanceof Error) {
|
|
37687
|
-
console.error(
|
|
34817
|
+
console.error(chalk23.red(`
|
|
37688
34818
|
\u2717 ${error51.message}
|
|
37689
34819
|
`));
|
|
37690
34820
|
}
|
|
@@ -37696,7 +34826,7 @@ org.action(async () => {
|
|
|
37696
34826
|
await orgCommand();
|
|
37697
34827
|
} catch (error51) {
|
|
37698
34828
|
if (error51 instanceof Error) {
|
|
37699
|
-
console.error(
|
|
34829
|
+
console.error(chalk23.red(`
|
|
37700
34830
|
\u2717 ${error51.message}
|
|
37701
34831
|
`));
|
|
37702
34832
|
}
|
|
@@ -37708,7 +34838,7 @@ program.command("connect <workspace-name>").description("Connect to a workspace
|
|
|
37708
34838
|
await connectCommand(workspaceName);
|
|
37709
34839
|
} catch (error51) {
|
|
37710
34840
|
if (error51 instanceof Error) {
|
|
37711
|
-
console.error(
|
|
34841
|
+
console.error(chalk23.red(`
|
|
37712
34842
|
\u2717 ${error51.message}
|
|
37713
34843
|
`));
|
|
37714
34844
|
}
|
|
@@ -37720,7 +34850,7 @@ program.command("code <workspace-name>").description("Open a workspace in VSCode
|
|
|
37720
34850
|
await codeCommand(workspaceName);
|
|
37721
34851
|
} catch (error51) {
|
|
37722
34852
|
if (error51 instanceof Error) {
|
|
37723
|
-
console.error(
|
|
34853
|
+
console.error(chalk23.red(`
|
|
37724
34854
|
\u2717 ${error51.message}
|
|
37725
34855
|
`));
|
|
37726
34856
|
}
|
|
@@ -37733,7 +34863,7 @@ config2.command("get <key>").description("Get a configuration value").action(asy
|
|
|
37733
34863
|
await configGetCommand(key);
|
|
37734
34864
|
} catch (error51) {
|
|
37735
34865
|
if (error51 instanceof Error) {
|
|
37736
|
-
console.error(
|
|
34866
|
+
console.error(chalk23.red(`
|
|
37737
34867
|
\u2717 ${error51.message}
|
|
37738
34868
|
`));
|
|
37739
34869
|
}
|
|
@@ -37745,7 +34875,7 @@ config2.command("set <key> <value>").description("Set a configuration value").ac
|
|
|
37745
34875
|
await configSetCommand(key, value);
|
|
37746
34876
|
} catch (error51) {
|
|
37747
34877
|
if (error51 instanceof Error) {
|
|
37748
|
-
console.error(
|
|
34878
|
+
console.error(chalk23.red(`
|
|
37749
34879
|
\u2717 ${error51.message}
|
|
37750
34880
|
`));
|
|
37751
34881
|
}
|
|
@@ -37757,7 +34887,7 @@ config2.command("list").description("List all configuration values").action(asyn
|
|
|
37757
34887
|
await configListCommand();
|
|
37758
34888
|
} catch (error51) {
|
|
37759
34889
|
if (error51 instanceof Error) {
|
|
37760
|
-
console.error(
|
|
34890
|
+
console.error(chalk23.red(`
|
|
37761
34891
|
\u2717 ${error51.message}
|
|
37762
34892
|
`));
|
|
37763
34893
|
}
|
|
@@ -37769,7 +34899,7 @@ program.command("list").description("List all replicas").option("-p, --page <pag
|
|
|
37769
34899
|
await replicaListCommand(options);
|
|
37770
34900
|
} catch (error51) {
|
|
37771
34901
|
if (error51 instanceof Error) {
|
|
37772
|
-
console.error(
|
|
34902
|
+
console.error(chalk23.red(`
|
|
37773
34903
|
\u2717 ${error51.message}
|
|
37774
34904
|
`));
|
|
37775
34905
|
}
|
|
@@ -37781,7 +34911,7 @@ program.command("get <id>").description("Get replica details by ID").action(asyn
|
|
|
37781
34911
|
await replicaGetCommand(id);
|
|
37782
34912
|
} catch (error51) {
|
|
37783
34913
|
if (error51 instanceof Error) {
|
|
37784
|
-
console.error(
|
|
34914
|
+
console.error(chalk23.red(`
|
|
37785
34915
|
\u2717 ${error51.message}
|
|
37786
34916
|
`));
|
|
37787
34917
|
}
|
|
@@ -37793,7 +34923,7 @@ program.command("create [name]").description("Create a new replica").option("-m,
|
|
|
37793
34923
|
await replicaCreateCommand(name, options);
|
|
37794
34924
|
} catch (error51) {
|
|
37795
34925
|
if (error51 instanceof Error) {
|
|
37796
|
-
console.error(
|
|
34926
|
+
console.error(chalk23.red(`
|
|
37797
34927
|
\u2717 ${error51.message}
|
|
37798
34928
|
`));
|
|
37799
34929
|
}
|
|
@@ -37805,7 +34935,7 @@ program.command("send <id>").description("Send a message to a replica").option("
|
|
|
37805
34935
|
await replicaSendCommand(id, options);
|
|
37806
34936
|
} catch (error51) {
|
|
37807
34937
|
if (error51 instanceof Error) {
|
|
37808
|
-
console.error(
|
|
34938
|
+
console.error(chalk23.red(`
|
|
37809
34939
|
\u2717 ${error51.message}
|
|
37810
34940
|
`));
|
|
37811
34941
|
}
|
|
@@ -37817,19 +34947,19 @@ program.command("delete <id>").description("Delete a replica").option("-f, --for
|
|
|
37817
34947
|
await replicaDeleteCommand(id, options);
|
|
37818
34948
|
} catch (error51) {
|
|
37819
34949
|
if (error51 instanceof Error) {
|
|
37820
|
-
console.error(
|
|
34950
|
+
console.error(chalk23.red(`
|
|
37821
34951
|
\u2717 ${error51.message}
|
|
37822
34952
|
`));
|
|
37823
34953
|
}
|
|
37824
34954
|
process.exit(1);
|
|
37825
34955
|
}
|
|
37826
34956
|
});
|
|
37827
|
-
program.command("read <id>").description("Read conversation history of a replica").option("-l, --limit <limit>", "Maximum number of events to return").option("-
|
|
34957
|
+
program.command("read <id>").description("Read conversation history of a replica").option("-l, --limit <limit>", "Maximum number of events to return, from the end").option("-b, --before-event <index>", "Read the page before this event index (see Has More output)").action(async (id, options) => {
|
|
37828
34958
|
try {
|
|
37829
34959
|
await replicaReadCommand(id, options);
|
|
37830
34960
|
} catch (error51) {
|
|
37831
34961
|
if (error51 instanceof Error) {
|
|
37832
|
-
console.error(
|
|
34962
|
+
console.error(chalk23.red(`
|
|
37833
34963
|
\u2717 ${error51.message}
|
|
37834
34964
|
`));
|
|
37835
34965
|
}
|
|
@@ -37842,7 +34972,7 @@ automation.command("list").description("List all automations").option("-p, --pag
|
|
|
37842
34972
|
await automationListCommand(options);
|
|
37843
34973
|
} catch (error51) {
|
|
37844
34974
|
if (error51 instanceof Error) {
|
|
37845
|
-
console.error(
|
|
34975
|
+
console.error(chalk23.red(`
|
|
37846
34976
|
\u2717 ${error51.message}
|
|
37847
34977
|
`));
|
|
37848
34978
|
}
|
|
@@ -37854,7 +34984,7 @@ automation.command("get <id>").description("Get automation details by ID").actio
|
|
|
37854
34984
|
await automationGetCommand(id);
|
|
37855
34985
|
} catch (error51) {
|
|
37856
34986
|
if (error51 instanceof Error) {
|
|
37857
|
-
console.error(
|
|
34987
|
+
console.error(chalk23.red(`
|
|
37858
34988
|
\u2717 ${error51.message}
|
|
37859
34989
|
`));
|
|
37860
34990
|
}
|
|
@@ -37869,7 +34999,7 @@ automation.command("create [name]").description("Create a new automation").optio
|
|
|
37869
34999
|
});
|
|
37870
35000
|
} catch (error51) {
|
|
37871
35001
|
if (error51 instanceof Error) {
|
|
37872
|
-
console.error(
|
|
35002
|
+
console.error(chalk23.red(`
|
|
37873
35003
|
\u2717 ${error51.message}
|
|
37874
35004
|
`));
|
|
37875
35005
|
}
|
|
@@ -37881,7 +35011,7 @@ automation.command("edit <id>").description("Edit an existing automation").optio
|
|
|
37881
35011
|
await automationEditCommand(id, options);
|
|
37882
35012
|
} catch (error51) {
|
|
37883
35013
|
if (error51 instanceof Error) {
|
|
37884
|
-
console.error(
|
|
35014
|
+
console.error(chalk23.red(`
|
|
37885
35015
|
\u2717 ${error51.message}
|
|
37886
35016
|
`));
|
|
37887
35017
|
}
|
|
@@ -37893,7 +35023,7 @@ automation.command("run <id>").description("Manually trigger an automation (cron
|
|
|
37893
35023
|
await automationRunCommand(id);
|
|
37894
35024
|
} catch (error51) {
|
|
37895
35025
|
if (error51 instanceof Error) {
|
|
37896
|
-
console.error(
|
|
35026
|
+
console.error(chalk23.red(`
|
|
37897
35027
|
\u2717 ${error51.message}
|
|
37898
35028
|
`));
|
|
37899
35029
|
}
|
|
@@ -37905,7 +35035,7 @@ automation.command("delete <id>").description("Delete an automation").option("-f
|
|
|
37905
35035
|
await automationDeleteCommand(id, options);
|
|
37906
35036
|
} catch (error51) {
|
|
37907
35037
|
if (error51 instanceof Error) {
|
|
37908
|
-
console.error(
|
|
35038
|
+
console.error(chalk23.red(`
|
|
37909
35039
|
\u2717 ${error51.message}
|
|
37910
35040
|
`));
|
|
37911
35041
|
}
|
|
@@ -37917,7 +35047,7 @@ automation.command("check <checkRunId>").description("Report this automation run
|
|
|
37917
35047
|
await automationCheckCommand(checkRunId, options);
|
|
37918
35048
|
} catch (error51) {
|
|
37919
35049
|
if (error51 instanceof Error) {
|
|
37920
|
-
console.error(
|
|
35050
|
+
console.error(chalk23.red(`
|
|
37921
35051
|
\u2717 ${error51.message}
|
|
37922
35052
|
`));
|
|
37923
35053
|
}
|
|
@@ -37929,7 +35059,7 @@ automation.action(async () => {
|
|
|
37929
35059
|
await automationListCommand({});
|
|
37930
35060
|
} catch (error51) {
|
|
37931
35061
|
if (error51 instanceof Error) {
|
|
37932
|
-
console.error(
|
|
35062
|
+
console.error(chalk23.red(`
|
|
37933
35063
|
\u2717 ${error51.message}
|
|
37934
35064
|
`));
|
|
37935
35065
|
}
|
|
@@ -37942,7 +35072,7 @@ repos.command("list").description("List all repositories").action(async () => {
|
|
|
37942
35072
|
await repositoriesListCommand();
|
|
37943
35073
|
} catch (error51) {
|
|
37944
35074
|
if (error51 instanceof Error) {
|
|
37945
|
-
console.error(
|
|
35075
|
+
console.error(chalk23.red(`
|
|
37946
35076
|
\u2717 ${error51.message}
|
|
37947
35077
|
`));
|
|
37948
35078
|
}
|
|
@@ -37954,7 +35084,7 @@ repos.action(async () => {
|
|
|
37954
35084
|
await repositoriesListCommand();
|
|
37955
35085
|
} catch (error51) {
|
|
37956
35086
|
if (error51 instanceof Error) {
|
|
37957
|
-
console.error(
|
|
35087
|
+
console.error(chalk23.red(`
|
|
37958
35088
|
\u2717 ${error51.message}
|
|
37959
35089
|
`));
|
|
37960
35090
|
}
|
|
@@ -37967,7 +35097,7 @@ environment.command("list").description("List all environments").action(async ()
|
|
|
37967
35097
|
await environmentListCommand();
|
|
37968
35098
|
} catch (error51) {
|
|
37969
35099
|
if (error51 instanceof Error) {
|
|
37970
|
-
console.error(
|
|
35100
|
+
console.error(chalk23.red(`
|
|
37971
35101
|
\u2717 ${error51.message}
|
|
37972
35102
|
`));
|
|
37973
35103
|
}
|
|
@@ -37979,7 +35109,7 @@ environment.command("get <id-or-name>").description('Get an environment by ID or
|
|
|
37979
35109
|
await environmentGetCommand(idOrName);
|
|
37980
35110
|
} catch (error51) {
|
|
37981
35111
|
if (error51 instanceof Error) {
|
|
37982
|
-
console.error(
|
|
35112
|
+
console.error(chalk23.red(`
|
|
37983
35113
|
\u2717 ${error51.message}
|
|
37984
35114
|
`));
|
|
37985
35115
|
}
|
|
@@ -37991,7 +35121,7 @@ environment.command("create [name]").description("Create a new environment").opt
|
|
|
37991
35121
|
await environmentCreateCommand(name, options);
|
|
37992
35122
|
} catch (error51) {
|
|
37993
35123
|
if (error51 instanceof Error) {
|
|
37994
|
-
console.error(
|
|
35124
|
+
console.error(chalk23.red(`
|
|
37995
35125
|
\u2717 ${error51.message}
|
|
37996
35126
|
`));
|
|
37997
35127
|
}
|
|
@@ -38003,7 +35133,7 @@ environment.command("edit <id-or-name>").description("Edit an environment").opti
|
|
|
38003
35133
|
await environmentEditCommand(idOrName, options);
|
|
38004
35134
|
} catch (error51) {
|
|
38005
35135
|
if (error51 instanceof Error) {
|
|
38006
|
-
console.error(
|
|
35136
|
+
console.error(chalk23.red(`
|
|
38007
35137
|
\u2717 ${error51.message}
|
|
38008
35138
|
`));
|
|
38009
35139
|
}
|
|
@@ -38015,7 +35145,7 @@ environment.command("delete <id-or-name>").description("Delete an environment").
|
|
|
38015
35145
|
await environmentDeleteCommand(idOrName, options);
|
|
38016
35146
|
} catch (error51) {
|
|
38017
35147
|
if (error51 instanceof Error) {
|
|
38018
|
-
console.error(
|
|
35148
|
+
console.error(chalk23.red(`
|
|
38019
35149
|
\u2717 ${error51.message}
|
|
38020
35150
|
`));
|
|
38021
35151
|
}
|
|
@@ -38028,7 +35158,7 @@ envVars.command("list <env>").description("List variables in an environment (val
|
|
|
38028
35158
|
await envVarsListCommand(env, options);
|
|
38029
35159
|
} catch (error51) {
|
|
38030
35160
|
if (error51 instanceof Error) {
|
|
38031
|
-
console.error(
|
|
35161
|
+
console.error(chalk23.red(`
|
|
38032
35162
|
\u2717 ${error51.message}
|
|
38033
35163
|
`));
|
|
38034
35164
|
}
|
|
@@ -38040,7 +35170,7 @@ envVars.command("set <env> <key> <value>").description("Create or update a varia
|
|
|
38040
35170
|
await envVarsSetCommand(env, key, value);
|
|
38041
35171
|
} catch (error51) {
|
|
38042
35172
|
if (error51 instanceof Error) {
|
|
38043
|
-
console.error(
|
|
35173
|
+
console.error(chalk23.red(`
|
|
38044
35174
|
\u2717 ${error51.message}
|
|
38045
35175
|
`));
|
|
38046
35176
|
}
|
|
@@ -38052,7 +35182,7 @@ envVars.command("delete <env> <key-or-id>").description("Delete a variable by ke
|
|
|
38052
35182
|
await envVarsDeleteCommand(env, keyOrId, options);
|
|
38053
35183
|
} catch (error51) {
|
|
38054
35184
|
if (error51 instanceof Error) {
|
|
38055
|
-
console.error(
|
|
35185
|
+
console.error(chalk23.red(`
|
|
38056
35186
|
\u2717 ${error51.message}
|
|
38057
35187
|
`));
|
|
38058
35188
|
}
|
|
@@ -38065,7 +35195,7 @@ envFiles.command("list <env>").description("List files in an environment").actio
|
|
|
38065
35195
|
await envFilesListCommand(env);
|
|
38066
35196
|
} catch (error51) {
|
|
38067
35197
|
if (error51 instanceof Error) {
|
|
38068
|
-
console.error(
|
|
35198
|
+
console.error(chalk23.red(`
|
|
38069
35199
|
\u2717 ${error51.message}
|
|
38070
35200
|
`));
|
|
38071
35201
|
}
|
|
@@ -38077,7 +35207,7 @@ envFiles.command("set <env> <destination-path>").description("Create or update a
|
|
|
38077
35207
|
await envFilesSetCommand(env, destinationPath, options);
|
|
38078
35208
|
} catch (error51) {
|
|
38079
35209
|
if (error51 instanceof Error) {
|
|
38080
|
-
console.error(
|
|
35210
|
+
console.error(chalk23.red(`
|
|
38081
35211
|
\u2717 ${error51.message}
|
|
38082
35212
|
`));
|
|
38083
35213
|
}
|
|
@@ -38089,7 +35219,7 @@ envFiles.command("delete <env> <path-or-id>").description("Delete a file by dest
|
|
|
38089
35219
|
await envFilesDeleteCommand(env, pathOrId, options);
|
|
38090
35220
|
} catch (error51) {
|
|
38091
35221
|
if (error51 instanceof Error) {
|
|
38092
|
-
console.error(
|
|
35222
|
+
console.error(chalk23.red(`
|
|
38093
35223
|
\u2717 ${error51.message}
|
|
38094
35224
|
`));
|
|
38095
35225
|
}
|
|
@@ -38102,7 +35232,7 @@ envStartHooks.command("get <env>").description("Show the active start hook for a
|
|
|
38102
35232
|
await envStartHookGetCommand(env);
|
|
38103
35233
|
} catch (error51) {
|
|
38104
35234
|
if (error51 instanceof Error) {
|
|
38105
|
-
console.error(
|
|
35235
|
+
console.error(chalk23.red(`
|
|
38106
35236
|
\u2717 ${error51.message}
|
|
38107
35237
|
`));
|
|
38108
35238
|
}
|
|
@@ -38114,7 +35244,7 @@ envStartHooks.command("save <env>").description("Save and activate a start hook
|
|
|
38114
35244
|
await envStartHookSaveCommand(env, options);
|
|
38115
35245
|
} catch (error51) {
|
|
38116
35246
|
if (error51 instanceof Error) {
|
|
38117
|
-
console.error(
|
|
35247
|
+
console.error(chalk23.red(`
|
|
38118
35248
|
\u2717 ${error51.message}
|
|
38119
35249
|
`));
|
|
38120
35250
|
}
|
|
@@ -38126,7 +35256,7 @@ envStartHooks.command("test <env>").description("Run a start hook in an isolated
|
|
|
38126
35256
|
await envStartHookTestCommand(env, options);
|
|
38127
35257
|
} catch (error51) {
|
|
38128
35258
|
if (error51 instanceof Error) {
|
|
38129
|
-
console.error(
|
|
35259
|
+
console.error(chalk23.red(`
|
|
38130
35260
|
\u2717 ${error51.message}
|
|
38131
35261
|
`));
|
|
38132
35262
|
}
|
|
@@ -38138,7 +35268,7 @@ envStartHooks.command("repository-hooks <env>").description("List per-repo start
|
|
|
38138
35268
|
await envStartHookRepositoryHooksCommand(env);
|
|
38139
35269
|
} catch (error51) {
|
|
38140
35270
|
if (error51 instanceof Error) {
|
|
38141
|
-
console.error(
|
|
35271
|
+
console.error(chalk23.red(`
|
|
38142
35272
|
\u2717 ${error51.message}
|
|
38143
35273
|
`));
|
|
38144
35274
|
}
|
|
@@ -38150,7 +35280,7 @@ environment.action(async () => {
|
|
|
38150
35280
|
await environmentListCommand();
|
|
38151
35281
|
} catch (error51) {
|
|
38152
35282
|
if (error51 instanceof Error) {
|
|
38153
|
-
console.error(
|
|
35283
|
+
console.error(chalk23.red(`
|
|
38154
35284
|
\u2717 ${error51.message}
|
|
38155
35285
|
`));
|
|
38156
35286
|
}
|
|
@@ -38162,7 +35292,7 @@ program.command("interact").alias("i").description("Launch the interactive termi
|
|
|
38162
35292
|
await interactiveCommand();
|
|
38163
35293
|
} catch (error51) {
|
|
38164
35294
|
if (error51 instanceof Error) {
|
|
38165
|
-
console.error(
|
|
35295
|
+
console.error(chalk23.red(`
|
|
38166
35296
|
\u2717 ${error51.message}
|
|
38167
35297
|
`));
|
|
38168
35298
|
}
|
|
@@ -38213,7 +35343,7 @@ if (isAgentMode()) {
|
|
|
38213
35343
|
await previewAddCommand(workspaceId, options);
|
|
38214
35344
|
} catch (error51) {
|
|
38215
35345
|
if (error51 instanceof Error) {
|
|
38216
|
-
console.error(
|
|
35346
|
+
console.error(chalk23.red(`
|
|
38217
35347
|
\u2717 ${error51.message}
|
|
38218
35348
|
`));
|
|
38219
35349
|
}
|
|
@@ -38225,7 +35355,7 @@ if (isAgentMode()) {
|
|
|
38225
35355
|
await previewListCommand(workspaceId);
|
|
38226
35356
|
} catch (error51) {
|
|
38227
35357
|
if (error51 instanceof Error) {
|
|
38228
|
-
console.error(
|
|
35358
|
+
console.error(chalk23.red(`
|
|
38229
35359
|
\u2717 ${error51.message}
|
|
38230
35360
|
`));
|
|
38231
35361
|
}
|
|
@@ -38237,7 +35367,7 @@ if (isAgentMode()) {
|
|
|
38237
35367
|
await previewRemoveCommand(workspaceId, options);
|
|
38238
35368
|
} catch (error51) {
|
|
38239
35369
|
if (error51 instanceof Error) {
|
|
38240
|
-
console.error(
|
|
35370
|
+
console.error(chalk23.red(`
|
|
38241
35371
|
\u2717 ${error51.message}
|
|
38242
35372
|
`));
|
|
38243
35373
|
}
|
|
@@ -38298,7 +35428,7 @@ if (isAgentMode()) {
|
|
|
38298
35428
|
await mediaUploadCommand(files, options);
|
|
38299
35429
|
} catch (error51) {
|
|
38300
35430
|
if (error51 instanceof Error) {
|
|
38301
|
-
console.error(
|
|
35431
|
+
console.error(chalk23.red(`
|
|
38302
35432
|
\u2717 ${error51.message}
|
|
38303
35433
|
`));
|
|
38304
35434
|
}
|
|
@@ -38309,7 +35439,7 @@ if (isAgentMode()) {
|
|
|
38309
35439
|
try {
|
|
38310
35440
|
await mediaShareCommand(mediaId);
|
|
38311
35441
|
} catch (error51) {
|
|
38312
|
-
if (error51 instanceof Error) console.error(
|
|
35442
|
+
if (error51 instanceof Error) console.error(chalk23.red(`
|
|
38313
35443
|
\u2717 ${error51.message}
|
|
38314
35444
|
`));
|
|
38315
35445
|
process.exit(1);
|
|
@@ -38319,7 +35449,7 @@ if (isAgentMode()) {
|
|
|
38319
35449
|
try {
|
|
38320
35450
|
await mediaRevokeCommand(mediaId);
|
|
38321
35451
|
} catch (error51) {
|
|
38322
|
-
if (error51 instanceof Error) console.error(
|
|
35452
|
+
if (error51 instanceof Error) console.error(chalk23.red(`
|
|
38323
35453
|
\u2717 ${error51.message}
|
|
38324
35454
|
`));
|
|
38325
35455
|
process.exit(1);
|
|
@@ -38330,7 +35460,7 @@ if (isAgentMode()) {
|
|
|
38330
35460
|
await mediaListCommand(options);
|
|
38331
35461
|
} catch (error51) {
|
|
38332
35462
|
if (error51 instanceof Error) {
|
|
38333
|
-
console.error(
|
|
35463
|
+
console.error(chalk23.red(`
|
|
38334
35464
|
\u2717 ${error51.message}
|
|
38335
35465
|
`));
|
|
38336
35466
|
}
|
|
@@ -38343,7 +35473,7 @@ if (isAgentMode()) {
|
|
|
38343
35473
|
await learningsReadCommand(options);
|
|
38344
35474
|
} catch (error51) {
|
|
38345
35475
|
if (error51 instanceof Error) {
|
|
38346
|
-
console.error(
|
|
35476
|
+
console.error(chalk23.red(`
|
|
38347
35477
|
\u2717 ${error51.message}
|
|
38348
35478
|
`));
|
|
38349
35479
|
}
|
|
@@ -38355,7 +35485,7 @@ if (isAgentMode()) {
|
|
|
38355
35485
|
await learningsAddCommand(options);
|
|
38356
35486
|
} catch (error51) {
|
|
38357
35487
|
if (error51 instanceof Error) {
|
|
38358
|
-
console.error(
|
|
35488
|
+
console.error(chalk23.red(`
|
|
38359
35489
|
\u2717 ${error51.message}
|
|
38360
35490
|
`));
|
|
38361
35491
|
}
|
|
@@ -38367,7 +35497,7 @@ if (isAgentMode()) {
|
|
|
38367
35497
|
await learningsUpdateCommand(id, options);
|
|
38368
35498
|
} catch (error51) {
|
|
38369
35499
|
if (error51 instanceof Error) {
|
|
38370
|
-
console.error(
|
|
35500
|
+
console.error(chalk23.red(`
|
|
38371
35501
|
\u2717 ${error51.message}
|
|
38372
35502
|
`));
|
|
38373
35503
|
}
|
|
@@ -38379,7 +35509,7 @@ if (isAgentMode()) {
|
|
|
38379
35509
|
await learningsDeleteCommand(id);
|
|
38380
35510
|
} catch (error51) {
|
|
38381
35511
|
if (error51 instanceof Error) {
|
|
38382
|
-
console.error(
|
|
35512
|
+
console.error(chalk23.red(`
|
|
38383
35513
|
\u2717 ${error51.message}
|
|
38384
35514
|
`));
|
|
38385
35515
|
}
|
|
@@ -38391,7 +35521,7 @@ if (isAgentMode()) {
|
|
|
38391
35521
|
await learningsWithdrawCommand(id);
|
|
38392
35522
|
} catch (error51) {
|
|
38393
35523
|
if (error51 instanceof Error) {
|
|
38394
|
-
console.error(
|
|
35524
|
+
console.error(chalk23.red(`
|
|
38395
35525
|
\u2717 ${error51.message}
|
|
38396
35526
|
`));
|
|
38397
35527
|
}
|
|
@@ -38404,7 +35534,7 @@ if (isAgentMode()) {
|
|
|
38404
35534
|
await mothershipSpawnCommand(options);
|
|
38405
35535
|
} catch (error51) {
|
|
38406
35536
|
if (error51 instanceof Error) {
|
|
38407
|
-
console.error(
|
|
35537
|
+
console.error(chalk23.red(`
|
|
38408
35538
|
\u2717 ${error51.message}
|
|
38409
35539
|
`));
|
|
38410
35540
|
}
|
|
@@ -38416,54 +35546,14 @@ if (isAgentMode()) {
|
|
|
38416
35546
|
await mothershipRelayCommand(options);
|
|
38417
35547
|
} catch (error51) {
|
|
38418
35548
|
if (error51 instanceof Error) {
|
|
38419
|
-
console.error(
|
|
35549
|
+
console.error(chalk23.red(`
|
|
38420
35550
|
\u2717 ${error51.message}
|
|
38421
35551
|
`));
|
|
38422
35552
|
}
|
|
38423
35553
|
process.exit(1);
|
|
38424
35554
|
}
|
|
38425
35555
|
});
|
|
38426
|
-
|
|
38427
|
-
const wrap = (fn) => async (...args) => {
|
|
38428
|
-
try {
|
|
38429
|
-
await fn(...args);
|
|
38430
|
-
} catch (error51) {
|
|
38431
|
-
if (error51 instanceof Error) {
|
|
38432
|
-
console.error(chalk24.red(`
|
|
38433
|
-
\u2717 ${error51.message}
|
|
38434
|
-
`));
|
|
38435
|
-
}
|
|
38436
|
-
process.exit(1);
|
|
38437
|
-
}
|
|
38438
|
-
};
|
|
38439
|
-
computer.command("info").description("Print the live noVNC viewer URL for the workspace desktop. The preview is registered automatically at engine startup, so this just looks it up \u2014 share the URL with the user to let them watch the desktop.").action(wrap(() => computerInfoCommand()));
|
|
38440
|
-
computer.command("status").description("Show which desktop services are running and the active preview URL").action(wrap(() => computerStatusCommand()));
|
|
38441
|
-
computer.command("screenshot <path>").description("Capture the current desktop to a PNG file. Use --raw or --grid for agent click planning; omit both for a branded shareable image.").option("--raw", "Save a 1:1 desktop capture with no branding, padding, or rounded corners").option("--grid [px]", "Overlay a coordinate grid on a 1:1 desktop capture. Default 100px.").action(wrap((path6, options) => computerScreenshotCommand(path6, options)));
|
|
38442
|
-
computer.command("observe <path>").description("Wait briefly for the desktop to settle, save a 1:1 screenshot, and print JSON screen context for agents.").option("--raw", "Save a 1:1 desktop capture with no coordinate grid").option("--grid [px]", "Overlay a coordinate grid on the 1:1 desktop capture. Default 100px.").option("--timeout <ms>", "Maximum time to wait for visual stability. Default 3000.").option("--stable-ms <ms>", "Required unchanged time before the screen is considered stable. Default 600.").option("--poll-ms <ms>", "Screenshot polling interval while waiting. Default 200.").action(wrap((path6, options) => computerObserveCommand(path6, options)));
|
|
38443
|
-
computer.command("browser").description("Print JSON for Chrome tabs launched through Replicas, including page titles and URLs.").option("--snapshot", "Include visible page text and interactive controls with bounding boxes").option("--limit <chars>", "Maximum body text characters per page snapshot. Default 4000.").option("--element-limit <n>", "Maximum controls per page snapshot. Default 80.").option("--target-id <id>", "Only include the Chrome page with this target id").option("--page <n>", "When multiple pages match, include the nth page. Default 0.").option("--title <text>", "Only include pages whose title contains text").option("--url <text>", "Only include pages whose URL contains text").action(wrap((options) => computerBrowserCommand(options)));
|
|
38444
|
-
computer.command("browser-state <path>").description("Wait for a Chrome page to settle, capture its viewport, and print a bounded accessibility state with actionable refs.").option("--full", "Return the full accessibility tree instead of a diff from the previous state").option("--limit <chars>", "Maximum visible text characters. Default 8000.").option("--element-limit <n>", "Maximum actionable elements. Default 120.").option("--timeout <ms>", "Maximum time to wait for state stability. Default 5000.").option("--stable-ms <ms>", "Required unchanged time before capture. Default 500.").option("--poll-ms <ms>", "State polling interval. Default 100.").option("--target-id <id>", "Target the Chrome page with this stable target id").option("--page <n>", "When multiple pages match, target the nth page. Default 0.").option("--title <text>", "Only target pages whose title contains text").option("--url <text>", "Only target pages whose URL contains text").action(wrap((path6, options) => computerBrowserStateCommand(path6, options)));
|
|
38445
|
-
computer.command("browser-batch <actions>").description("Run an ordered, fail-fast JSON array of browser actions through one Chrome session.").option("--target-id <id>", "Target the Chrome page with this stable target id").option("--page <n>", "When multiple pages match, target the nth page. Default 0.").option("--title <text>", "Only target pages whose title contains text").option("--url <text>", "Only target pages whose URL contains text").action(wrap((actions, options) => computerBrowserBatchCommand(actions, options)));
|
|
38446
|
-
computer.command("browser-click [text]").description("Click a Chrome element by ref from browser-state, or fall back to visible control text.").option("--ref <id>", "Click the backend DOM node ref from the latest browser state").option("--exact", "Require an exact text match instead of substring matching").option("--index <n>", "When multiple controls match, click the nth match. Default 0.").option("--button <button>", "Mouse button for ref clicks: left, middle, or right. Default left.").option("--double", "Double-click a ref target").option("--target-id <id>", "Only target the Chrome page with this target id").option("--page <n>", "When multiple pages match, target the nth page. Default 0.").option("--title <text>", "Only target pages whose title contains text").option("--url <text>", "Only target pages whose URL contains text").action(wrap((text, options) => computerBrowserClickCommand(text, options)));
|
|
38447
|
-
computer.command("browser-fill [field] [value]").description("Fill a Chrome field by ref from browser-state, or fall back to visible field text.").option("--ref <id>", "Fill the backend DOM node ref from the latest browser state").option("--exact", "Require an exact field match instead of substring matching").option("--index <n>", "When multiple fields match, fill the nth match. Default 0.").option("--target-id <id>", "Only target the Chrome page with this target id").option("--page <n>", "When multiple pages match, target the nth page. Default 0.").option("--title <text>", "Only target pages whose title contains text").option("--url <text>", "Only target pages whose URL contains text").action(wrap((field2, value, options) => {
|
|
38448
|
-
const resolvedValue = value ?? (options.ref ? field2 : void 0);
|
|
38449
|
-
const resolvedField = value === void 0 && options.ref ? void 0 : field2;
|
|
38450
|
-
if (resolvedValue === void 0) throw new Error("Provide <field> <value>, or --ref <id> <value>");
|
|
38451
|
-
return computerBrowserFillCommand(resolvedField, resolvedValue, options);
|
|
38452
|
-
}));
|
|
38453
|
-
computer.command("browser-scroll <direction>").description("Send trusted wheel input to a Chrome element ref or the center of the target page.").option("--ref <id>", "Scroll from the backend DOM node ref from the latest browser state").option("--amount <px>", "Scroll distance in CSS pixels. Default 600.").option("--target-id <id>", "Target the Chrome page with this stable target id").option("--page <n>", "When multiple pages match, target the nth page. Default 0.").option("--title <text>", "Only target pages whose title contains text").option("--url <text>", "Only target pages whose URL contains text").action(wrap((direction, options) => computerBrowserScrollCommand(direction, options)));
|
|
38454
|
-
computer.command("browser-key <combo>").description("Press a key or modifier chord in the target Chrome page with trusted browser input.").option("--target-id <id>", "Target the Chrome page with this stable target id").option("--page <n>", "When multiple pages match, target the nth page. Default 0.").option("--title <text>", "Only target pages whose title contains text").option("--url <text>", "Only target pages whose URL contains text").action(wrap((combo, options) => computerBrowserKeyCommand(combo, options)));
|
|
38455
|
-
computer.command("browser-type <text>").description("Type literal text into the focused field in the target Chrome page with trusted browser input.").option("--target-id <id>", "Target the Chrome page with this stable target id").option("--page <n>", "When multiple pages match, target the nth page. Default 0.").option("--title <text>", "Only target pages whose title contains text").option("--url <text>", "Only target pages whose URL contains text").action(wrap((text, options) => computerBrowserTypeCommand(text, options)));
|
|
38456
|
-
computer.command("browser-wait <text>").description("Wait until Chrome page title, URL, body text, or controls match <text>.").option("--mode <mode>", "Where to match: any, text, title, url, or control. Default any.").option("--exact", "Require an exact match instead of substring matching").option("--timeout <ms>", "Maximum time to wait. Default 10000.").option("--poll-ms <ms>", "Polling interval. Default 250.").option("--target-id <id>", "Only target the Chrome page with this target id").option("--page <n>", "When multiple pages match, target the nth page. Default 0.").option("--title <text>", "Only target pages whose title contains text").option("--url <text>", "Only target pages whose URL contains text").action(wrap((text, options) => computerBrowserWaitCommand(text, options)));
|
|
38457
|
-
computer.command("click <x> <y>").description("Move to (x, y) and click. Coordinates are pixels or percentages like 50% 50%.").option("-b, --button <n>", "Mouse button (1=left, 2=middle, 3=right). Default 1.").option("--double", "Double-click instead of single-click").option("--modifiers <mods>", "Hold modifier keys during the click, e.g. ctrl or ctrl+shift").action(wrap((x, y, options) => computerClickCommand(x, y, options)));
|
|
38458
|
-
computer.command("move <x> <y>").description("Move the mouse to (x, y) without clicking. Coordinates are pixels or percentages like 50% 50%.").action(wrap((x, y) => computerMoveCommand(x, y)));
|
|
38459
|
-
computer.command("type <text>").description("Type a literal string into the focused field. Use `key` for key combos like ctrl+l.").option("--delay <ms>", "Per-character delay in ms (default 12 \u2248 80 wpm)").action(wrap((text, options) => computerTypeCommand(text, options)));
|
|
38460
|
-
computer.command("key <combo>").description("Press a key combo, e.g. Return, Escape, ctrl+l, ctrl+shift+t. Same syntax as xdotool key.").action(wrap((combo) => computerKeyCommand(combo)));
|
|
38461
|
-
computer.command("scroll <direction>").description("Scroll up | down | left | right. Optionally provide --x / --y to hover before scrolling.").option("--amount <n>", "Wheel ticks (default 3)").option("--x <x>", "Hover x before scrolling. Pixels or percentage.").option("--y <y>", "Hover y before scrolling. Pixels or percentage.").action(wrap((direction, options) => computerScrollCommand(direction, options)));
|
|
38462
|
-
computer.command("drag <fromX> <fromY> <toX> <toY>").description("Press the left mouse button at (fromX, fromY), drag to (toX, toY), release. Coordinates are pixels or percentages.").action(wrap((fx, fy, tx, ty) => computerDragCommand(fx, fy, tx, ty)));
|
|
38463
|
-
computer.command("launch <app> [args...]").description("Launch an app on the workspace display. Aliases: chrome, chromium, firefox, terminal.").action(wrap((app, args) => computerLaunchCommand(app, args)));
|
|
38464
|
-
const record2 = computer.command("record").description("Screen-record the workspace display to an MP4 (60fps libx264).");
|
|
38465
|
-
record2.command("start <path>").description("Start recording to <path>. Output is fragmented MP4 (safe if the workspace dies mid-record).").option("--fps <n>", "Frame rate (default 60)").action(wrap((path6, options) => computerRecordStartCommand(path6, options)));
|
|
38466
|
-
record2.command("stop").description("Stop the active recording and finalize the MP4. Prints the output path.").action(wrap(() => computerRecordStopCommand()));
|
|
35556
|
+
program.command("computer").description("Drive the workspace Linux desktop through replicas-computer");
|
|
38467
35557
|
const allowed = /* @__PURE__ */ new Set([
|
|
38468
35558
|
"init",
|
|
38469
35559
|
"whoami",
|
|
@@ -38486,6 +35576,18 @@ if (isAgentMode()) {
|
|
|
38486
35576
|
cmds.push(...kept);
|
|
38487
35577
|
}
|
|
38488
35578
|
async function main() {
|
|
35579
|
+
if (process.argv[2] === "computer" && isAgentMode()) {
|
|
35580
|
+
const result = spawnSync("replicas-computer", process.argv.slice(3), { stdio: "inherit" });
|
|
35581
|
+
if (result.error) {
|
|
35582
|
+
console.error(chalk23.red(`
|
|
35583
|
+
\u2717 replicas-computer is unavailable in this workspace image
|
|
35584
|
+
`));
|
|
35585
|
+
process.exitCode = 1;
|
|
35586
|
+
return;
|
|
35587
|
+
}
|
|
35588
|
+
process.exitCode = result.status ?? 1;
|
|
35589
|
+
return;
|
|
35590
|
+
}
|
|
38489
35591
|
startUpdateCheck(CLI_VERSION);
|
|
38490
35592
|
program.parse();
|
|
38491
35593
|
}
|