refactor(browser): dedupe browser and cli command wiring

This commit is contained in:
Peter Steinberger
2026-03-02 21:29:10 +00:00
parent 58e9ca2fb6
commit 067855e623
12 changed files with 807 additions and 1481 deletions

View File

@@ -13,6 +13,31 @@ export function registerBrowserElementCommands(
browser: Command,
parentOpts: (cmd: Command) => BrowserParentOpts,
) {
const runElementAction = async (params: {
cmd: Command;
body: Record<string, unknown>;
successMessage: string | ((result: unknown) => string);
timeoutMs?: number;
}): Promise<void> => {
const { parent, profile } = resolveBrowserActionContext(params.cmd, parentOpts);
try {
const result = await callBrowserAct({
parent,
profile,
body: params.body,
timeoutMs: params.timeoutMs,
});
const successMessage =
typeof params.successMessage === "function"
? params.successMessage(result)
: params.successMessage;
logBrowserActionResult(parent, result, successMessage);
} catch (err) {
defaultRuntime.error(danger(String(err)));
defaultRuntime.exit(1);
}
};
browser
.command("click")
.description("Click an element by ref from snapshot")
@@ -22,7 +47,6 @@ export function registerBrowserElementCommands(
.option("--button <left|right|middle>", "Mouse button to use")
.option("--modifiers <list>", "Comma-separated modifiers (Shift,Alt,Meta)")
.action(async (ref: string | undefined, opts, cmd) => {
const { parent, profile } = resolveBrowserActionContext(cmd, parentOpts);
const refValue = requireRef(ref);
if (!refValue) {
return;
@@ -33,25 +57,22 @@ export function registerBrowserElementCommands(
.map((v: string) => v.trim())
.filter(Boolean)
: undefined;
try {
const result = await callBrowserAct<{ url?: string }>({
parent,
profile,
body: {
kind: "click",
ref: refValue,
targetId: opts.targetId?.trim() || undefined,
doubleClick: Boolean(opts.double),
button: opts.button?.trim() || undefined,
modifiers,
},
});
const suffix = result.url ? ` on ${result.url}` : "";
logBrowserActionResult(parent, result, `clicked ref ${refValue}${suffix}`);
} catch (err) {
defaultRuntime.error(danger(String(err)));
defaultRuntime.exit(1);
}
await runElementAction({
cmd,
body: {
kind: "click",
ref: refValue,
targetId: opts.targetId?.trim() || undefined,
doubleClick: Boolean(opts.double),
button: opts.button?.trim() || undefined,
modifiers,
},
successMessage: (result) => {
const url = (result as { url?: unknown }).url;
const suffix = typeof url === "string" && url ? ` on ${url}` : "";
return `clicked ref ${refValue}${suffix}`;
},
});
});
browser
@@ -63,29 +84,22 @@ export function registerBrowserElementCommands(
.option("--slowly", "Type slowly (human-like)", false)
.option("--target-id <id>", "CDP target id (or unique prefix)")
.action(async (ref: string | undefined, text: string, opts, cmd) => {
const { parent, profile } = resolveBrowserActionContext(cmd, parentOpts);
const refValue = requireRef(ref);
if (!refValue) {
return;
}
try {
const result = await callBrowserAct({
parent,
profile,
body: {
kind: "type",
ref: refValue,
text,
submit: Boolean(opts.submit),
slowly: Boolean(opts.slowly),
targetId: opts.targetId?.trim() || undefined,
},
});
logBrowserActionResult(parent, result, `typed into ref ${refValue}`);
} catch (err) {
defaultRuntime.error(danger(String(err)));
defaultRuntime.exit(1);
}
await runElementAction({
cmd,
body: {
kind: "type",
ref: refValue,
text,
submit: Boolean(opts.submit),
slowly: Boolean(opts.slowly),
targetId: opts.targetId?.trim() || undefined,
},
successMessage: `typed into ref ${refValue}`,
});
});
browser
@@ -94,18 +108,11 @@ export function registerBrowserElementCommands(
.argument("<key>", "Key to press (e.g. Enter)")
.option("--target-id <id>", "CDP target id (or unique prefix)")
.action(async (key: string, opts, cmd) => {
const { parent, profile } = resolveBrowserActionContext(cmd, parentOpts);
try {
const result = await callBrowserAct({
parent,
profile,
body: { kind: "press", key, targetId: opts.targetId?.trim() || undefined },
});
logBrowserActionResult(parent, result, `pressed ${key}`);
} catch (err) {
defaultRuntime.error(danger(String(err)));
defaultRuntime.exit(1);
}
await runElementAction({
cmd,
body: { kind: "press", key, targetId: opts.targetId?.trim() || undefined },
successMessage: `pressed ${key}`,
});
});
browser
@@ -114,18 +121,11 @@ export function registerBrowserElementCommands(
.argument("<ref>", "Ref id from snapshot")
.option("--target-id <id>", "CDP target id (or unique prefix)")
.action(async (ref: string, opts, cmd) => {
const { parent, profile } = resolveBrowserActionContext(cmd, parentOpts);
try {
const result = await callBrowserAct({
parent,
profile,
body: { kind: "hover", ref, targetId: opts.targetId?.trim() || undefined },
});
logBrowserActionResult(parent, result, `hovered ref ${ref}`);
} catch (err) {
defaultRuntime.error(danger(String(err)));
defaultRuntime.exit(1);
}
await runElementAction({
cmd,
body: { kind: "hover", ref, targetId: opts.targetId?.trim() || undefined },
successMessage: `hovered ref ${ref}`,
});
});
browser
@@ -137,28 +137,22 @@ export function registerBrowserElementCommands(
Number(v),
)
.action(async (ref: string | undefined, opts, cmd) => {
const { parent, profile } = resolveBrowserActionContext(cmd, parentOpts);
const refValue = requireRef(ref);
if (!refValue) {
return;
}
try {
const result = await callBrowserAct({
parent,
profile,
body: {
kind: "scrollIntoView",
ref: refValue,
targetId: opts.targetId?.trim() || undefined,
timeoutMs: Number.isFinite(opts.timeoutMs) ? opts.timeoutMs : undefined,
},
timeoutMs: Number.isFinite(opts.timeoutMs) ? opts.timeoutMs : undefined,
});
logBrowserActionResult(parent, result, `scrolled into view: ${refValue}`);
} catch (err) {
defaultRuntime.error(danger(String(err)));
defaultRuntime.exit(1);
}
const timeoutMs = Number.isFinite(opts.timeoutMs) ? opts.timeoutMs : undefined;
await runElementAction({
cmd,
body: {
kind: "scrollIntoView",
ref: refValue,
targetId: opts.targetId?.trim() || undefined,
timeoutMs,
},
timeoutMs,
successMessage: `scrolled into view: ${refValue}`,
});
});
browser
@@ -168,23 +162,16 @@ export function registerBrowserElementCommands(
.argument("<endRef>", "End ref id")
.option("--target-id <id>", "CDP target id (or unique prefix)")
.action(async (startRef: string, endRef: string, opts, cmd) => {
const { parent, profile } = resolveBrowserActionContext(cmd, parentOpts);
try {
const result = await callBrowserAct({
parent,
profile,
body: {
kind: "drag",
startRef,
endRef,
targetId: opts.targetId?.trim() || undefined,
},
});
logBrowserActionResult(parent, result, `dragged ${startRef}${endRef}`);
} catch (err) {
defaultRuntime.error(danger(String(err)));
defaultRuntime.exit(1);
}
await runElementAction({
cmd,
body: {
kind: "drag",
startRef,
endRef,
targetId: opts.targetId?.trim() || undefined,
},
successMessage: `dragged ${startRef}${endRef}`,
});
});
browser
@@ -194,22 +181,15 @@ export function registerBrowserElementCommands(
.argument("<values...>", "Option values to select")
.option("--target-id <id>", "CDP target id (or unique prefix)")
.action(async (ref: string, values: string[], opts, cmd) => {
const { parent, profile } = resolveBrowserActionContext(cmd, parentOpts);
try {
const result = await callBrowserAct({
parent,
profile,
body: {
kind: "select",
ref,
values,
targetId: opts.targetId?.trim() || undefined,
},
});
logBrowserActionResult(parent, result, `selected ${values.join(", ")}`);
} catch (err) {
defaultRuntime.error(danger(String(err)));
defaultRuntime.exit(1);
}
await runElementAction({
cmd,
body: {
kind: "select",
ref,
values,
targetId: opts.targetId?.trim() || undefined,
},
successMessage: `selected ${values.join(", ")}`,
});
});
}

View File

@@ -18,6 +18,36 @@ async function normalizeUploadPaths(paths: string[]): Promise<string[]> {
return result.paths;
}
async function runBrowserPostAction<T>(params: {
parent: BrowserParentOpts;
profile: string | undefined;
path: string;
body: Record<string, unknown>;
timeoutMs: number;
describeSuccess: (result: T) => string;
}): Promise<void> {
try {
const result = await callBrowserRequest<T>(
params.parent,
{
method: "POST",
path: params.path,
query: params.profile ? { profile: params.profile } : undefined,
body: params.body,
},
{ timeoutMs: params.timeoutMs },
);
if (params.parent?.json) {
defaultRuntime.log(JSON.stringify(result, null, 2));
return;
}
defaultRuntime.log(params.describeSuccess(result));
} catch (err) {
defaultRuntime.error(danger(String(err)));
defaultRuntime.exit(1);
}
}
export function registerBrowserFilesAndDownloadsCommands(
browser: Command,
parentOpts: (cmd: Command) => BrowserParentOpts,
@@ -35,31 +65,19 @@ export function registerBrowserFilesAndDownloadsCommands(
request: { path: string; body: Record<string, unknown> },
) => {
const { parent, profile } = resolveBrowserActionContext(cmd, parentOpts);
try {
const { timeoutMs, targetId } = resolveTimeoutAndTarget(opts);
const result = await callBrowserRequest<{ download: { path: string } }>(
parent,
{
method: "POST",
path: request.path,
query: profile ? { profile } : undefined,
body: {
...request.body,
targetId,
timeoutMs,
},
},
{ timeoutMs: timeoutMs ?? 20000 },
);
if (parent?.json) {
defaultRuntime.log(JSON.stringify(result, null, 2));
return;
}
defaultRuntime.log(`downloaded: ${shortenHomePath(result.download.path)}`);
} catch (err) {
defaultRuntime.error(danger(String(err)));
defaultRuntime.exit(1);
}
const { timeoutMs, targetId } = resolveTimeoutAndTarget(opts);
await runBrowserPostAction<{ download: { path: string } }>({
parent,
profile,
path: request.path,
body: {
...request.body,
targetId,
timeoutMs,
},
timeoutMs: timeoutMs ?? 20000,
describeSuccess: (result) => `downloaded: ${shortenHomePath(result.download.path)}`,
});
};
browser
@@ -80,35 +98,23 @@ export function registerBrowserFilesAndDownloadsCommands(
)
.action(async (paths: string[], opts, cmd) => {
const { parent, profile } = resolveBrowserActionContext(cmd, parentOpts);
try {
const normalizedPaths = await normalizeUploadPaths(paths);
const { timeoutMs, targetId } = resolveTimeoutAndTarget(opts);
const result = await callBrowserRequest<{ download: { path: string } }>(
parent,
{
method: "POST",
path: "/hooks/file-chooser",
query: profile ? { profile } : undefined,
body: {
paths: normalizedPaths,
ref: opts.ref?.trim() || undefined,
inputRef: opts.inputRef?.trim() || undefined,
element: opts.element?.trim() || undefined,
targetId,
timeoutMs,
},
},
{ timeoutMs: timeoutMs ?? 20000 },
);
if (parent?.json) {
defaultRuntime.log(JSON.stringify(result, null, 2));
return;
}
defaultRuntime.log(`upload armed for ${paths.length} file(s)`);
} catch (err) {
defaultRuntime.error(danger(String(err)));
defaultRuntime.exit(1);
}
const normalizedPaths = await normalizeUploadPaths(paths);
const { timeoutMs, targetId } = resolveTimeoutAndTarget(opts);
await runBrowserPostAction({
parent,
profile,
path: "/hooks/file-chooser",
body: {
paths: normalizedPaths,
ref: opts.ref?.trim() || undefined,
inputRef: opts.inputRef?.trim() || undefined,
element: opts.element?.trim() || undefined,
targetId,
timeoutMs,
},
timeoutMs: timeoutMs ?? 20000,
describeSuccess: () => `upload armed for ${paths.length} file(s)`,
});
});
browser
@@ -177,31 +183,19 @@ export function registerBrowserFilesAndDownloadsCommands(
defaultRuntime.exit(1);
return;
}
try {
const { timeoutMs, targetId } = resolveTimeoutAndTarget(opts);
const result = await callBrowserRequest(
parent,
{
method: "POST",
path: "/hooks/dialog",
query: profile ? { profile } : undefined,
body: {
accept,
promptText: opts.prompt?.trim() || undefined,
targetId,
timeoutMs,
},
},
{ timeoutMs: timeoutMs ?? 20000 },
);
if (parent?.json) {
defaultRuntime.log(JSON.stringify(result, null, 2));
return;
}
defaultRuntime.log("dialog armed");
} catch (err) {
defaultRuntime.error(danger(String(err)));
defaultRuntime.exit(1);
}
const { timeoutMs, targetId } = resolveTimeoutAndTarget(opts);
await runBrowserPostAction({
parent,
profile,
path: "/hooks/dialog",
body: {
accept,
promptText: opts.prompt?.trim() || undefined,
targetId,
timeoutMs,
},
timeoutMs: timeoutMs ?? 20000,
describeSuccess: () => "dialog armed",
});
});
}