#![allow(clippy::doc_markdown)]
#![allow(dead_code)]
use std::path::PathBuf;
use clap::{ArgAction, Args, Parser, Subcommand, ValueEnum};
use clap_complete::Shell;
use crate::coords::CoordValue;
#[derive(Parser)]
#[command(
name = "agentchrome",
version,
about = "Browser automation and lighthouse auditing via the Chrome DevTools Protocol",
long_about = "agentchrome is a command-line tool for browser automation via the Chrome DevTools \
Protocol (CDP). It provides subcommands for connecting to Chrome/Chromium instances, \
managing tabs, navigating pages, inspecting the DOM, executing JavaScript, monitoring \
console output, intercepting network requests, simulating user interactions, filling forms, \
emulating devices, and collecting performance metrics. Lighthouse-backed auditing is \
available through `agentchrome audit lighthouse` and requires the lighthouse npm \
package.\n\n\
Designed for AI agents and shell scripting, every subcommand produces structured JSON \
output on stdout and structured JSON errors on stderr. Global flags control connection \
settings, output format, and target tab selection.",
after_long_help = "\
QUICK START:
# Connect to a running Chrome instance
agentchrome connect
# Launch a new headless Chrome and connect
agentchrome connect --launch --headless
# List open tabs and navigate to a URL
agentchrome tabs list
agentchrome navigate https://example.com
# Take a full-page screenshot
agentchrome page screenshot --full-page --file shot.png
# Execute JavaScript and get the result
agentchrome js exec \"document.title\"
# Capture the accessibility tree and fill a form field
agentchrome page snapshot
agentchrome form fill s5 \"hello@example.com\"
# Monitor console output in real time
agentchrome console follow --timeout 5000
EXIT CODES:
0 Success
1 General error (invalid arguments, internal failure)
2 Connection error (Chrome not running, session expired)
3 Target error (tab not found, no page targets)
4 Timeout error (navigation or trace timeout)
5 Protocol error (CDP protocol failure, dialog handling error)
ERROR HANDLING:
Every non-zero exit emits exactly one JSON object on stderr. The stable
shape is:
{\"error\": \"<human-readable message>\", \"code\": <1..5>}
Some paths enrich the payload with optional fields (stable fields remain
present) — for example form fill against a non-fillable element:
{\"error\": \"...\", \"code\": 1, \"kind\": \"not_fillable\",
\"element_type\": {\"tag\": \"div\", \"role\": null},
\"suggested_alternatives\": [\"'agentchrome interact click'\"]}
Exit-code meanings: 0=success, 1=general, 2=connection, 3=target,
4=timeout, 5=protocol. Success-path stdout remains JSON and stderr stays
empty; stderr is reserved for the error object only.
ENVIRONMENT VARIABLES:
AGENTCHROME_PORT CDP port number (default: 9222)
AGENTCHROME_HOST CDP host address (default: 127.0.0.1)
AGENTCHROME_TIMEOUT Default command timeout in milliseconds
AGENTCHROME_CONFIG Path to configuration file",
term_width = 100
)]
pub struct Cli {
#[command(flatten)]
pub global: GlobalOpts,
#[command(subcommand)]
pub command: Command,
}
#[derive(Args)]
pub struct GlobalOpts {
#[arg(long, global = true, env = "AGENTCHROME_PORT")]
pub port: Option<u16>,
#[arg(
long,
default_value = "127.0.0.1",
global = true,
env = "AGENTCHROME_HOST"
)]
pub host: String,
#[arg(long, global = true)]
pub ws_url: Option<String>,
#[arg(long, global = true, env = "AGENTCHROME_TIMEOUT")]
pub timeout: Option<u64>,
#[arg(long, global = true)]
pub tab: Option<String>,
#[arg(long, global = true, conflicts_with = "tab")]
pub page_id: Option<String>,
#[arg(long, global = true)]
pub auto_dismiss_dialogs: bool,
#[arg(long, global = true, env = "AGENTCHROME_CONFIG")]
pub config: Option<PathBuf>,
#[arg(
long = "keepalive-interval",
value_name = "MS",
value_parser = clap::value_parser!(u64),
env = "AGENTCHROME_KEEPALIVE_INTERVAL",
global = true,
conflicts_with = "no_keepalive"
)]
pub keepalive_interval: Option<u64>,
#[arg(long = "no-keepalive", global = true)]
pub no_keepalive: bool,
#[command(flatten)]
pub output: OutputFormat,
}
impl GlobalOpts {
const DEFAULT_PORT: u16 = 9222;
#[must_use]
pub fn port_or_default(&self) -> u16 {
self.port.unwrap_or(Self::DEFAULT_PORT)
}
}
#[allow(clippy::struct_excessive_bools)]
#[derive(Args)]
pub struct OutputFormat {
#[arg(long, global = true, conflicts_with_all = ["pretty", "plain"])]
pub json: bool,
#[arg(long, global = true, conflicts_with_all = ["json", "plain"])]
pub pretty: bool,
#[arg(long, global = true, conflicts_with_all = ["json", "pretty"])]
pub plain: bool,
#[arg(long, global = true, value_parser = parse_nonzero_usize)]
pub large_response_threshold: Option<usize>,
}
fn parse_nonzero_usize(s: &str) -> Result<usize, String> {
let val: usize = s.parse().map_err(|e| format!("{e}"))?;
if val == 0 {
return Err("threshold must be greater than 0".to_string());
}
Ok(val)
}
pub const SESSION_FILE_PATH_UNIX: &str = "~/.agentchrome/session.json";
pub const SESSION_FILE_PATH_WINDOWS: &str = "%USERPROFILE%\\.agentchrome\\session.json";
pub const CONNECTION_PRECEDENCE: &[&str] = &[
"--ws-url",
"--port",
"AGENTCHROME_PORT",
"session.json",
"default port 9222",
];
pub const DEFAULT_MARKDOWN_MAX_INPUT_BYTES: usize = 1_048_576;
#[derive(Subcommand)]
pub enum Command {
#[command(
long_about = "Connect to a running Chrome/Chromium instance via the Chrome DevTools \
Protocol, or launch a new one. Tests the connection and prints browser metadata \
(browser version, WebSocket URL, user agent). The session is persisted to a \
local file so subsequent commands reuse the same connection.",
after_long_help = "\
SESSION FILE:
Connection state is persisted to a per-user session file so subsequent
CLI invocations can reuse the same Chrome automatically:
Unix: ~/.agentchrome/session.json
Windows: %USERPROFILE%\\.agentchrome\\session.json
RESOLUTION PRECEDENCE (highest → lowest):
1. --ws-url
2. --port
3. AGENTCHROME_PORT (env var)
4. session.json
5. default port 9222
EXAMPLES:
# Connect to Chrome on the default port (9222)
agentchrome connect
# Launch a new headless Chrome instance
agentchrome connect --launch --headless
# Cross-invocation auto-discovery (no flags in shell B):
# shell A
agentchrome connect --launch --headless
# shell B, later
agentchrome tabs list
# Connect to a specific port
agentchrome connect --port 9333
# Check connection status (exits 0 whether or not a session exists)
agentchrome connect --status
# Disconnect and remove session file
agentchrome connect --disconnect
# Run a long command with a custom keep-alive interval
agentchrome --keepalive-interval 60000 console follow
# Disable keep-alive entirely
agentchrome --no-keepalive page snapshot --json"
)]
Connect(ConnectArgs),
#[command(
long_about = "Tab management commands: list open tabs, create new tabs, close tabs, and \
activate (focus) a specific tab. Each operation returns structured JSON with tab IDs \
and metadata.",
after_long_help = "\
EXAMPLES:
# List all open tabs
agentchrome tabs list
# Open a new tab and get its ID
agentchrome tabs create https://example.com
# Close tabs by ID
agentchrome tabs close ABC123 DEF456
# Activate a specific tab
agentchrome tabs activate ABC123"
)]
Tabs(TabsArgs),
#[command(
long_about = "Navigate to URLs, reload pages, go back/forward in history, and wait for \
navigation events. Supports waiting for load, DOMContentLoaded, or network idle.",
after_long_help = "\
EXAMPLES:
# Navigate to a URL and wait for page load
agentchrome navigate https://example.com
# Navigate and wait for network idle
agentchrome navigate https://example.com --wait-until networkidle
# Go back in browser history
agentchrome navigate back
# Reload the current page, bypassing cache
agentchrome navigate reload --ignore-cache"
)]
Navigate(NavigateArgs),
#[command(
long_about = "Inspect the current page: capture screenshots (full page or element), \
extract visible text, dump the accessibility tree, or search for text/elements on \
the page.",
after_long_help = "\
EXAMPLES:
# Extract all visible text from the page
agentchrome page text
# Capture the accessibility tree (assigns UIDs to elements)
agentchrome page snapshot
# Take a full-page screenshot
agentchrome page screenshot --full-page --file page.png
# Find elements by text
agentchrome page find \"Sign in\"
# Resize the viewport
agentchrome page resize 1280x720"
)]
Page(PageArgs),
#[command(
long_about = "Query and manipulate the DOM: select elements by CSS selector or XPath, \
get/set attributes and text, read outerHTML, inspect computed styles, navigate the \
element tree, and remove elements. Target elements by node ID (from 'dom select'), \
snapshot UID (from 'page snapshot'), or CSS selector (prefixed with 'css:').",
after_long_help = "\
EXAMPLES:
# Select elements by CSS selector
agentchrome dom select \"h1\"
# Select by XPath
agentchrome dom select \"//a[@href]\" --xpath
# Get an element's attribute
agentchrome dom get-attribute s3 href
# Read element text
agentchrome dom get-text css:h1
# Set an attribute
agentchrome dom set-attribute s5 class \"highlight\"
# View the DOM tree
agentchrome dom tree --depth 3"
)]
Dom(DomArgs),
#[command(
long_about = "Execute JavaScript expressions or scripts in the page context. Returns \
the result as structured JSON. Supports both synchronous expressions and async \
functions.",
after_long_help = "\
EXAMPLES:
# Get the page title
agentchrome js exec \"document.title\"
# Execute a script file
agentchrome js exec --file script.js
# Run code on a specific element (by UID from snapshot)
agentchrome js exec --uid s3 \"(el) => el.textContent\"
# Read from stdin
echo 'document.URL' | agentchrome js exec -"
)]
Js(JsArgs),
#[command(
long_about = "Read and monitor browser console messages (log, warn, error, info). \
Can capture existing messages or stream new messages in real time.",
after_long_help = "\
EXAMPLES:
# Read recent console messages
agentchrome console read
# Show only error messages
agentchrome console read --errors-only
# Stream console messages in real time
agentchrome console follow
# Stream errors for 10 seconds
agentchrome console follow --errors-only --timeout 10000"
)]
Console(ConsoleArgs),
#[command(
long_about = "Monitor and intercept network requests. List recent requests, filter by \
URL pattern or resource type, capture request/response bodies, and stream requests \
in real time.",
after_long_help = "\
EXAMPLES:
# List recent network requests
agentchrome network list
# Filter by resource type
agentchrome network list --type xhr,fetch
# Get details of a specific request
agentchrome network get 42
# Stream network requests in real time
agentchrome network follow --url api.example.com"
)]
Network(NetworkArgs),
#[command(
long_about = "Simulate user interactions: click elements, type text, press key \
combinations, scroll the page, hover over elements, and perform drag-and-drop \
operations. Target elements by UID (from 'page snapshot') or CSS selector \
(prefixed with 'css:').",
after_long_help = "\
EXAMPLES:
# Click an element by UID
agentchrome interact click s5
# Click by CSS selector
agentchrome interact click css:#submit-btn
# Type text into the focused element
agentchrome interact type \"Hello, world!\"
# Press a key combination
agentchrome interact key Control+A
# Scroll down one viewport height
agentchrome interact scroll"
)]
Interact(InteractArgs),
#[command(
long_about = "Fill in form fields, select dropdown options, toggle checkboxes, and clear \
fields. Supports targeting fields by UID (from accessibility snapshot) or CSS \
selector (prefixed with 'css:'). Run 'page snapshot' first to discover field UIDs.",
after_long_help = "\
EXAMPLES:
# Fill a field by UID (from page snapshot)
agentchrome form fill s5 \"hello@example.com\"
# Fill by CSS selector
agentchrome form fill css:#email \"user@example.com\"
# Fill multiple fields at once
agentchrome form fill-many '[{\"target\":\"s5\",\"value\":\"Alice\"},{\"target\":\"s7\",\"value\":\"alice@example.com\"}]'
# Clear a field
agentchrome form clear s5
# Upload a file
agentchrome form upload s10 ./photo.jpg"
)]
Form(FormArgs),
#[command(
long_about = "Emulate different devices, screen sizes, and network conditions. Set \
custom user agents, viewport dimensions, device scale factor, and network throttling \
profiles.",
after_long_help = "\
EXAMPLES:
# Emulate a mobile device
agentchrome emulate set --viewport 375x667 --device-scale 2 --mobile
# Simulate slow 3G network
agentchrome emulate set --network 3g
# Force dark mode
agentchrome emulate set --color-scheme dark
# Check current emulation settings
agentchrome emulate status
# Clear all emulation overrides
agentchrome emulate reset"
)]
Emulate(EmulateArgs),
#[command(
long_about = "Collect performance metrics, capture trace files, measure page load timing, \
and analyze runtime performance. Outputs metrics as structured JSON for analysis.",
after_long_help = "\
EXAMPLES:
# Quick Core Web Vitals measurement
agentchrome perf vitals
# Record a trace until Ctrl+C
agentchrome perf record
# Record a trace for 5 seconds
agentchrome perf record --duration 5000
# Record with page reload
agentchrome perf record --reload --duration 5000
# Analyze a trace for render-blocking resources
agentchrome perf analyze RenderBlocking --trace-file trace.json"
)]
Perf(PerfArgs),
#[command(
long_about = "Manage browser cookies via the Chrome DevTools Protocol. List cookies \
for the current page or all cookies, set new cookies with optional flags, delete \
specific cookies by name, or clear all cookies. Provides full access to HttpOnly \
and Secure cookies that are not accessible via document.cookie.",
after_long_help = "\
EXAMPLES:
# List cookies for the current page
agentchrome cookie list
# List all cookies (not scoped to current URL)
agentchrome cookie list --all
# List cookies filtered by domain
agentchrome cookie list --domain example.com
# Set a cookie
agentchrome cookie set session_id abc123 --domain example.com
# Set a secure, HttpOnly cookie with expiry
agentchrome cookie set token xyz --domain example.com --secure --http-only --same-site Strict --expires 1735689600
# Delete a specific cookie
agentchrome cookie delete session_id --domain example.com
# Clear all cookies
agentchrome cookie clear"
)]
Cookie(CookieArgs),
#[command(
long_about = "Detect and handle browser JavaScript dialogs (alert, confirm, prompt, \
beforeunload). Query whether a dialog is open, accept or dismiss it, and provide \
prompt text. Useful for automation scripts that need to respond to dialogs \
programmatically.",
after_long_help = "\
EXAMPLES:
# Check if a dialog is open
agentchrome dialog info
# Accept an alert or confirm dialog
agentchrome dialog handle accept
# Dismiss a dialog
agentchrome dialog handle dismiss
# Accept a prompt with text
agentchrome dialog handle accept --text \"my input\""
)]
Dialog(DialogArgs),
#[command(
long_about = "Discover and control HTML5 audio and video elements on the current page. \
List all media elements with playback state, play/pause individual elements, seek to \
a specific time or to the end of the media. Supports targeting by index or CSS \
selector, bulk operations with --all, and frame-scoped media control with --frame.",
after_long_help = "\
EXAMPLES:
# List all media elements on the page
agentchrome media list
# Play a media element by index
agentchrome media play 0
# Pause a media element
agentchrome media pause 0
# Seek to 15.5 seconds
agentchrome media seek 0 15.5
# Seek all media elements to end (skip narration gates)
agentchrome media seek-end --all
# List media elements inside an iframe
agentchrome media --frame 0 list
# Play a media element by CSS selector
agentchrome media play css:audio.narration"
)]
Media(MediaArgs),
#[command(
long_about = "Run external audits against the current browser page. Currently supports \
Google Lighthouse for measuring performance, accessibility, SEO, best practices, \
and PWA scores. Connects Lighthouse to the managed Chrome session via the CDP port \
and returns structured JSON category scores on stdout. Requires the `lighthouse` \
CLI (see 'audit lighthouse --help' for installation).",
after_long_help = "\
EXAMPLES:
# Run a full Lighthouse audit on the current page
agentchrome audit lighthouse
# Audit a specific URL
agentchrome audit lighthouse https://example.com
# Only measure performance and accessibility
agentchrome audit lighthouse --only performance,accessibility
# Save the full Lighthouse report to a file
agentchrome audit lighthouse --output-file report.json"
)]
Audit(AuditArgs),
#[command(
long_about = "Scan a page for automation challenges — iframes, overlay blockers, shadow \
DOM, canvas/WebGL rendering, media playback gates, and framework-specific interaction \
quirks — plus named-pattern matches (e.g., Storyline acc-blocker, SCORM player, React \
portal) with actionable agentchrome command suggestions. Accepts a URL to \
navigate-then-analyze, or `--current` to analyze the already-loaded page in place.\n\n\
OUTPUT SCHEMA (JSON on stdout):\n\
{\n\
\"url\": string,\n\
\"scope\": \"diagnosed\" | \"current\",\n\
\"challenges\": [{category, severity, summary, details, suggestion?}],\n\
\"patterns\": [{name, matched, confidence, evidence, suggestion}],\n\
\"summary\": {challengeCount, patternMatchCount, hasHighSeverity, straightforward}\n\
}\n\n\
EXIT CODES: 0 success; 1 general/arg errors; 2 connection; 3 target; 4 timeout; \
5 protocol.\n\n\
Note: <url> and --current are mutually exclusive. Exactly one must be provided.",
after_long_help = "\
EXAMPLES:
# Navigate to a URL and diagnose it for automation challenges
agentchrome diagnose https://example.com/course
# Diagnose the already-loaded page in the active tab (no navigation)
agentchrome diagnose --current
# Extract strategy suggestions using jq
agentchrome diagnose --current | jq -r '.patterns[].suggestion'
# Diagnose with network-idle wait strategy
agentchrome diagnose https://app.example.com --wait-until networkidle"
)]
Diagnose(DiagnoseArgs),
#[command(
long_about = "Convert the current browser page or one raw HTML source into cleaned \
Markdown for agentic scraping and research workflows. By default, the command \
reads the active browser page and returns structured JSON with markdown, source, \
and metadata fields. Use --file, --stdin, or --url to convert raw HTML without \
requiring a browser connection. Cleanup removes common page chrome and boilerplate, \
prefers primary content regions, preserves headings, lists, links, code blocks, \
blockquotes, separators, and content tables, and routes large responses through \
the shared large-response temp-file gate.",
after_long_help = "\
EXAMPLES:
# Convert the current browser page to JSON
agentchrome markdown --json
# Emit only the Markdown body for the current page
agentchrome markdown --plain
# Convert a local HTML file and resolve relative links
agentchrome markdown --file article.html --base-url https://example.com/docs/
# Convert HTML from stdin
cat article.html | agentchrome markdown --stdin --base-url https://example.com/
# Fetch and convert a URL
agentchrome markdown --url https://example.com/article
# Scope extraction to a CSS selector
agentchrome markdown --file article.html --selector main
# Preserve text but strip link destinations
agentchrome markdown --file article.html --strip-links
# Include useful images as Markdown image references
agentchrome markdown --file article.html --include-images"
)]
Markdown(MarkdownArgs),
#[command(
long_about = "Install, update, uninstall, or list agentchrome skill files for agentic \
coding tools (Claude Code, Windsurf, Aider, Continue.dev, GitHub Copilot, Cursor, \
Gemini CLI, Codex). \
The skill file is a minimal signpost that tells the AI agent what agentchrome is \
and how to discover its capabilities via the CLI's built-in help system. Without \
--tool, install writes to every detected supported agent and update refreshes every \
stale installed AgentChrome skill. Use --tool to target exactly one tool.",
after_long_help = "\
EXAMPLES:
# Install into every detected supported agent
agentchrome skill install
# Install for a specific tool
agentchrome skill install --tool claude-code
# Install for Codex
agentchrome skill install --tool codex
# List supported tools and installation status
agentchrome skill list
# Update every stale installed AgentChrome skill
agentchrome skill update
# Update one explicit target
agentchrome skill update --tool claude-code
# Remove an installed skill
agentchrome skill uninstall --tool claude-code"
)]
Skill(SkillArgs),
#[command(
long_about = "Manage the agentchrome configuration file. Show the resolved configuration \
from all sources, create a default config file, or display the active config file path. \
Config files use TOML format and are searched in priority order: --config flag, \
$AGENTCHROME_CONFIG env var, project-local, XDG config dir, home directory.",
after_long_help = "\
EXAMPLES:
# Show the resolved configuration
agentchrome config show
# Create a default config file
agentchrome config init
# Create a config at a custom path
agentchrome config init --path ./my-config.toml
# Show the active config file path
agentchrome config path"
)]
Config(ConfigArgs),
#[command(
long_about = "Generate shell completion scripts for tab-completion of commands, flags, \
and enum values. Pipe the output to the appropriate file for your shell.",
after_long_help = "\
EXAMPLES:
# Bash
agentchrome completions bash > /etc/bash_completion.d/agentchrome
# Zsh
agentchrome completions zsh > ~/.zfunc/_agentchrome
# Fish
agentchrome completions fish > ~/.config/fish/completions/agentchrome.fish
# PowerShell
agentchrome completions powershell >> $PROFILE
# Elvish
agentchrome completions elvish >> ~/.elvish/rc.elv"
)]
Completions(CompletionsArgs),
#[command(
long_about = "Show usage examples for agentchrome commands. Without arguments, lists all \
command groups with a brief description and one example each. With a command name, \
shows detailed examples for that specific command group. With \"strategies\" as the \
first positional, shows scenario-based interaction strategy guides — use \
`agentchrome examples strategies` to list all guides, or \
`agentchrome examples strategies <name>` to see the full guide for one strategy.",
after_long_help = "\
EXAMPLES:
# List all command groups with summary examples
agentchrome examples
# Show detailed examples for the navigate command
agentchrome examples navigate
# List all interaction strategy guides
agentchrome examples strategies
# Show the iframe strategy guide
agentchrome examples strategies iframes
# Get all strategies as JSON (for programmatic use)
agentchrome examples strategies --json
# Get a single strategy as JSON
agentchrome examples strategies iframes --json
# Pretty-printed JSON output
agentchrome examples --pretty"
)]
Examples(ExamplesArgs),
#[command(
long_about = "Output a machine-readable manifest of agentchrome CLI capabilities. \
Without arguments, returns a lightweight listing (name + description per command) \
for cheap discovery. With a command name, returns the full detail descriptor for \
that command — subcommands, args, flags, types. The manifest is generated at \
runtime from the clap command tree, so it is always in sync with the binary.",
after_long_help = "\
EXAMPLES:
# Summary listing of all commands (progressive disclosure)
agentchrome capabilities
# Summary listing as JSON
agentchrome capabilities --json
# Full detail for one command
agentchrome capabilities page
# Full detail for one command, as JSON
agentchrome capabilities page --json
# Pretty-printed for readability
agentchrome capabilities --pretty"
)]
Capabilities(CapabilitiesArgs),
#[command(
long_about = "Display man pages for agentchrome commands. Without arguments, displays \
the main agentchrome man page. With a subcommand name, displays the man page for \
that specific command. Output is in roff format, suitable for piping to a pager.",
after_long_help = "\
EXAMPLES:
# Display the main agentchrome man page
agentchrome man
# Display the man page for the connect command
agentchrome man connect
# Display the man page for the tabs command
agentchrome man tabs
# Pipe to a pager
agentchrome man navigate | less"
)]
Man(ManArgs),
#[command(
long_about = "Execute a JSON batch script composed of agentchrome commands, conditional \
branches, and loops against the active CDP session. The script runs sequentially \
and emits a structured JSON result array on stdout. Use 'script run -' to read \
a script from stdin.\n\n\
SCRIPT FORMAT (JSON v1):\n\
{ \"commands\": [\n\
{ \"cmd\": [\"navigate\", \"https://example.com\"] },\n\
{ \"cmd\": [\"js\", \"exec\", \"document.title\"], \"bind\": \"title\" },\n\
{ \"if\": \"$vars.title.includes('Example')\",\n\
\"then\": [{ \"cmd\": [\"page\", \"snapshot\"] }],\n\
\"else\": [] },\n\
{ \"loop\": { \"count\": 3 }, \"body\": [{ \"cmd\": [\"interact\", \"click\", \"s1\"] }] }\n\
] }",
after_long_help = "\
EXAMPLES:
# Run a script file
agentchrome script run workflow.json
# Read a script from stdin
echo '{\"commands\":[{\"cmd\":[\"navigate\",\"https://example.com\"]}]}' | agentchrome script run -
# Stop at the first failure
agentchrome script run --fail-fast workflow.json
# Validate a script without dispatching any commands
agentchrome script run --dry-run workflow.json
# Chain navigate and js exec to extract the page title
agentchrome script run - <<'EOF'
{\"commands\":[
{\"cmd\":[\"navigate\",\"https://example.com\"]},
{\"cmd\":[\"js\",\"exec\",\"document.title\"],\"bind\":\"title\"}
]}
EOF"
)]
Script(ScriptArgs),
}
#[derive(Args)]
pub struct ScriptArgs {
#[command(subcommand)]
pub sub: ScriptSubcommand,
}
#[derive(Subcommand)]
pub enum ScriptSubcommand {
#[command(
long_about = "Run a JSON batch script against the active CDP session. The script file \
should contain a JSON object with a 'commands' array. Each command is an argv-style \
array of strings. Use '-' as the file path to read from stdin.\n\n\
Supports conditional branching (if/then/else), count and while loops, and \
variable binding (bind: \"name\") with $vars.name substitution in later steps.",
after_long_help = "\
EXAMPLES:
# Run a script file
agentchrome script run workflow.json
# Read a script from stdin (- means stdin)
cat workflow.json | agentchrome script run -
# Stop at the first failing step and exit non-zero
agentchrome script run --fail-fast workflow.json
# Validate the script without dispatching commands (offline)
agentchrome script run --dry-run workflow.json
# Pretty-print the result JSON
agentchrome script run --pretty workflow.json"
)]
Run(ScriptRunArgs),
}
#[derive(Args)]
pub struct ScriptRunArgs {
#[arg(
value_name = "FILE",
help = "Path to a JSON script file; use '-' to read from stdin"
)]
pub file: String,
#[arg(
long,
help = "Stop execution at the first failing step and exit non-zero"
)]
pub fail_fast: bool,
#[arg(
long,
help = "Parse and validate the script without executing any commands"
)]
pub dry_run: bool,
}
#[derive(Debug, Clone, Copy, ValueEnum)]
pub enum ChromeChannel {
Stable,
Canary,
Beta,
Dev,
}
#[derive(Args)]
pub struct TabsArgs {
#[command(subcommand)]
pub command: TabsCommand,
}
#[derive(Subcommand)]
pub enum TabsCommand {
#[command(
long_about = "List all open browser tabs. Returns JSON with each tab's ID, title, URL, \
and type. By default, only page tabs are shown; use --all to include internal \
Chrome pages (chrome://, chrome-extension://).",
after_long_help = "\
EXAMPLES:
# List page tabs
agentchrome tabs list
# Include internal Chrome pages
agentchrome tabs list --all"
)]
List(TabsListArgs),
#[command(
long_about = "Create a new browser tab. Optionally specify a URL to open; defaults to \
about:blank. Returns JSON with the new tab's ID and URL. Use --background to open \
the tab without switching focus to it.",
after_long_help = "\
EXAMPLES:
# Open a blank tab
agentchrome tabs create
# Open a URL
agentchrome tabs create https://example.com
# Open in the background
agentchrome tabs create https://example.com --background"
)]
Create(TabsCreateArgs),
#[command(
long_about = "Close one or more browser tabs by their IDs. Accepts multiple tab IDs \
as arguments. Returns JSON confirming which tabs were closed. Cannot close the \
last remaining tab (Chrome requires at least one open tab).",
after_long_help = "\
EXAMPLES:
# Close a single tab
agentchrome tabs close ABC123
# Close multiple tabs
agentchrome tabs close ABC123 DEF456 GHI789"
)]
Close(TabsCloseArgs),
#[command(
long_about = "Activate (bring to front) a specific browser tab by its ID. The tab \
becomes the active target for subsequent commands. Returns JSON confirming the \
activated tab.",
after_long_help = "\
EXAMPLES:
# Activate a tab by ID
agentchrome tabs activate ABC123
# Activate silently
agentchrome tabs activate ABC123 --quiet"
)]
Activate(TabsActivateArgs),
}
#[derive(Args)]
pub struct TabsListArgs {
#[arg(long)]
pub all: bool,
}
#[derive(Args)]
pub struct TabsCreateArgs {
pub url: Option<String>,
#[arg(long)]
pub background: bool,
}
#[derive(Args)]
pub struct TabsCloseArgs {
pub targets: Vec<String>,
#[arg(long, hide = true, action = ArgAction::Append, value_name = "ID")]
pub tab: Vec<String>,
}
#[derive(Args)]
pub struct TabsActivateArgs {
pub target: String,
#[arg(long)]
pub quiet: bool,
}
#[derive(Args)]
#[command(args_conflicts_with_subcommands = true)]
pub struct NavigateArgs {
#[command(subcommand)]
pub command: Option<NavigateCommand>,
#[command(flatten)]
pub url_args: NavigateUrlArgs,
}
#[derive(Subcommand)]
pub enum NavigateCommand {
#[command(
long_about = "Navigate back one step in the browser's session history, equivalent to \
clicking the browser's back button. Returns JSON with the new URL after navigation.",
after_long_help = "\
EXAMPLES:
# Go back
agentchrome navigate back"
)]
Back,
#[command(
long_about = "Navigate forward one step in the browser's session history, equivalent to \
clicking the browser's forward button. Only works if the user previously navigated \
back. Returns JSON with the new URL after navigation.",
after_long_help = "\
EXAMPLES:
# Go forward
agentchrome navigate forward"
)]
Forward,
#[command(
long_about = "Reload the current page. Use --ignore-cache to bypass the browser cache \
and force a full reload from the server. Returns JSON with the page URL after reload.",
after_long_help = "\
EXAMPLES:
# Reload the page
agentchrome navigate reload
# Reload bypassing cache
agentchrome navigate reload --ignore-cache"
)]
Reload(NavigateReloadArgs),
}
#[derive(Args)]
pub struct NavigateUrlArgs {
pub url: Option<String>,
#[arg(long, value_enum, default_value_t = WaitUntil::Load)]
pub wait_until: WaitUntil,
#[arg(long)]
pub timeout: Option<u64>,
#[arg(long)]
pub ignore_cache: bool,
#[arg(long)]
pub wait_for_selector: Option<String>,
}
#[derive(Args)]
pub struct NavigateReloadArgs {
#[arg(long)]
pub ignore_cache: bool,
}
#[derive(Args)]
pub struct PageArgs {
#[arg(long)]
pub frame: Option<String>,
#[command(subcommand)]
pub command: PageCommand,
}
#[derive(Subcommand)]
pub enum PageCommand {
#[command(
long_about = "Extract the visible text content from the current page or a specific \
element. Returns the text as a plain string. Useful for reading page content \
without HTML markup.",
after_long_help = "\
EXAMPLES:
# Get all visible text
agentchrome page text
# Get text from a specific element
agentchrome page text --selector \"#main-content\""
)]
Text(PageTextArgs),
#[command(
long_about = "Capture the accessibility tree (AX tree) of the current page. Each \
interactive element is assigned a UID (e.g., s1, s2, s3) that can be used with \
'interact', 'form', and 'js exec --uid' commands. Use --verbose to include \
additional properties like checked, disabled, and level.",
after_long_help = "\
EXAMPLES:
# Capture the accessibility tree
agentchrome page snapshot
# Verbose output with extra properties
agentchrome page snapshot --verbose
# Save to a file
agentchrome page snapshot --file snapshot.txt"
)]
Snapshot(PageSnapshotArgs),
#[command(
long_about = "Search for elements on the page by text content, CSS selector, or \
accessibility role. Returns matching elements with their UIDs, roles, and names. \
By default, performs a case-insensitive substring match; use --exact for exact \
matching.",
after_long_help = "\
EXAMPLES:
# Find elements by text
agentchrome page find \"Sign in\"
# Find by CSS selector
agentchrome page find --selector \"button.primary\"
# Find by accessibility role
agentchrome page find --role button
# Exact text match with limit
agentchrome page find \"Submit\" --exact --limit 1"
)]
Find(PageFindArgs),
#[command(
long_about = "Capture a screenshot of the current page, a specific element, or a \
viewport region. Supports PNG (default), JPEG, and WebP formats. Use --full-page \
to capture the entire scrollable page, --selector or --uid to capture a specific \
element, or --clip to capture a region. Note: --full-page conflicts with \
--selector, --uid, and --clip.",
after_long_help = "\
EXAMPLES:
# Screenshot the visible viewport
agentchrome page screenshot --file shot.png
# Full-page screenshot
agentchrome page screenshot --full-page --file full.png
# Screenshot a specific element by UID
agentchrome page screenshot --uid s3 --file element.png
# JPEG format with quality
agentchrome page screenshot --format jpeg --quality 80 --file shot.jpg"
)]
Screenshot(PageScreenshotArgs),
#[command(
long_about = "Resize the browser viewport to the specified dimensions. The size is \
given as WIDTHxHEIGHT in pixels (e.g., 1280x720). Useful for testing responsive \
layouts. See also: 'emulate set --viewport' for device emulation.",
after_long_help = "\
EXAMPLES:
# Resize to 1280x720
agentchrome page resize 1280x720
# Mobile viewport
agentchrome page resize 375x667"
)]
Resize(PageResizeArgs),
#[command(
long_about = "Query a single element's state by accessibility UID (from 'page snapshot') \
or CSS selector. Returns role, name, tag name, bounding box, accessibility properties, \
and viewport visibility as JSON.",
after_long_help = "\
EXAMPLES:
# Query by UID
agentchrome page element s10
# Query by CSS selector
agentchrome page element \"css:#checkout\"
# Plain text output
agentchrome page element s10 --plain"
)]
Element(PageElementArgs),
#[command(
arg_required_else_help = true,
long_about = "Wait until a specified condition is met on the current page. Supports \
waiting for a URL to match a glob pattern, text to appear, a CSS selector to match, \
network activity to settle, or a JavaScript expression to evaluate to truthy. \
Exactly one condition must be specified. The command blocks until the condition is \
satisfied or the timeout is reached.",
after_long_help = "\
EXAMPLES:
# Wait for URL to match a glob pattern
agentchrome page wait --url \"*/dashboard*\"
# Wait for text to appear
agentchrome page wait --text \"Products\"
# Wait for a CSS selector to match
agentchrome page wait --selector \"#results-table\"
# Wait for at least 5 elements to match a selector
agentchrome page wait --selector \".item\" --count 5
# Wait for network to settle
agentchrome page wait --network-idle
# Wait for a JavaScript expression to become truthy
agentchrome page wait --js-expression \"document.querySelector('.btn').disabled === false\"
# Wait for audio element to finish playing
agentchrome page wait --js-expression \"document.querySelector('audio').ended\"
# Custom timeout and poll interval
agentchrome page wait --text \"loaded\" --timeout 5000 --interval 200"
)]
Wait(PageWaitArgs),
#[command(
long_about = "List all frames in the current page, including iframes and frameset frames. \
Returns a JSON array with each frame's index, ID, URL, name, security origin, \
dimensions, and nesting depth. Use the index with --frame on other commands to target \
a specific frame.",
after_long_help = "\
EXAMPLES:
# List all frames
agentchrome page frames
# Pretty-printed output
agentchrome page --pretty frames"
)]
Frames,
#[command(
long_about = "List all workers associated with the current page, including Service Workers, \
Shared Workers, and dedicated Web Workers. Returns a JSON array with each worker's \
index, target ID, type, script URL, and status.",
after_long_help = "\
EXAMPLES:
# List all workers
agentchrome page workers"
)]
Workers,
#[command(
name = "hittest",
long_about = "Hit test at the given viewport coordinates to identify which element \
receives a click event. Returns the actual hit target, any intercepting overlay \
elements, and the full z-index stack at those coordinates. Useful for debugging \
failed click interactions caused by invisible overlays.",
after_long_help = "\
EXAMPLES:
# Hit test at viewport coordinates
agentchrome page hittest 100 200
# Hit test within a specific iframe
agentchrome page --frame 1 hittest 50 50"
)]
HitTest(PageHitTestArgs),
#[command(
long_about = "Analyze the structural composition of the current page. Returns a JSON \
report covering iframe hierarchy, detected frontend frameworks, interactive element \
counts, media elements, overlay blockers, and shadow DOM presence. Useful for \
understanding an unfamiliar page before choosing an automation strategy.",
after_long_help = "\
EXAMPLES:
# Analyze current page
agentchrome page analyze
# Analyze within a specific iframe
agentchrome page --frame 1 analyze"
)]
Analyze,
#[command(
long_about = "Resolve a CSS selector or snapshot UID to frame-local and page-global \
bounding-box and center coordinates. Returns a JSON object with 'frame', \
'frameLocal', 'page', and 'frameOffset' fields. Useful for verifying what \
coordinates a subsequent 'interact click-at --relative-to' would dispatch, \
and for diagnosing coordinate mismatches when working with iframes.\n\n\
Selector forms:\n \
css:#submit — standard CSS selector (prefix 'css:')\n \
s7 — snapshot UID from a prior 'page snapshot' run\n\n\
Use --frame to resolve in a specific iframe's coordinate space.",
after_long_help = "\
EXAMPLES:
# Resolve coordinates for a main-frame element
agentchrome page coords --selector \"css:#submit\"
# Resolve coordinates for an element inside frame 1
agentchrome page --frame 1 coords --selector \"css:#inner\"
# Resolve by snapshot UID
agentchrome page coords --selector s7"
)]
Coords(PageCoordsArgs),
}
#[derive(Debug, Clone, Copy, ValueEnum, Default)]
pub enum ScreenshotFormat {
#[default]
Png,
Jpeg,
Webp,
}
#[derive(Args)]
pub struct PageScreenshotArgs {
#[arg(long)]
pub full_page: bool,
#[arg(long)]
pub selector: Option<String>,
#[arg(long)]
pub uid: Option<String>,
#[arg(long)]
pub scroll_container: Option<String>,
#[arg(long, value_enum, default_value_t = ScreenshotFormat::Png)]
pub format: ScreenshotFormat,
#[arg(long, value_parser = clap::value_parser!(u8).range(0..=100))]
pub quality: Option<u8>,
#[arg(long)]
pub file: Option<PathBuf>,
#[arg(long)]
pub clip: Option<String>,
}
#[derive(Args)]
pub struct PageTextArgs {
#[arg(long)]
pub selector: Option<String>,
#[arg(long)]
pub deep: bool,
}
#[allow(clippy::struct_excessive_bools)]
#[derive(Args)]
pub struct PageSnapshotArgs {
#[arg(long)]
pub verbose: bool,
#[arg(long)]
pub file: Option<PathBuf>,
#[arg(long)]
pub compact: bool,
#[arg(long)]
pub pierce_shadow: bool,
#[arg(long)]
pub include_iframes: bool,
}
#[derive(Args)]
pub struct PageFindArgs {
pub query: Option<String>,
#[arg(long)]
pub selector: Option<String>,
#[arg(long)]
pub role: Option<String>,
#[arg(long)]
pub exact: bool,
#[arg(long, default_value_t = 10)]
pub limit: usize,
}
#[derive(Args)]
pub struct PerfArgs {
#[command(subcommand)]
pub command: PerfCommand,
}
#[derive(Subcommand)]
pub enum PerfCommand {
#[command(
long_about = "Record a performance trace in a single long-running session. The trace \
captures JavaScript execution, layout, paint, network, and other browser activity. \
Recording continues until you press Ctrl+C or the --duration timeout elapses. \
Use --reload to reload the page before recording. The trace is saved to a JSON \
file that can be opened in Chrome DevTools or analyzed with 'perf analyze'.",
after_long_help = "\
EXAMPLES:
# Record until Ctrl+C
agentchrome perf record
# Record for 5 seconds
agentchrome perf record --duration 5000
# Record with page reload
agentchrome perf record --reload --duration 5000
# Save to a specific file
agentchrome perf record --file my-trace.json"
)]
Record(PerfRecordArgs),
#[command(
long_about = "Analyze a previously saved trace file for a specific performance insight. \
Available insights: DocumentLatency (document request timing), LCPBreakdown (Largest \
Contentful Paint phases), RenderBlocking (render-blocking resources), LongTasks \
(JavaScript tasks > 50ms). Returns structured JSON with the analysis results.",
after_long_help = "\
EXAMPLES:
# Analyze LCP breakdown
agentchrome perf analyze LCPBreakdown --trace-file trace.json
# Find render-blocking resources
agentchrome perf analyze RenderBlocking --trace-file trace.json
# Identify long tasks
agentchrome perf analyze LongTasks --trace-file trace.json"
)]
Analyze(PerfAnalyzeArgs),
#[command(
long_about = "Perform a quick Core Web Vitals measurement. Automatically starts a \
trace, reloads the page, collects vitals (LCP, FID, CLS), and stops the trace. \
Returns structured JSON with the web vitals metrics.",
after_long_help = "\
EXAMPLES:
# Measure web vitals
agentchrome perf vitals
# Save the underlying trace file
agentchrome perf vitals --file vitals-trace.json"
)]
Vitals(PerfVitalsArgs),
}
#[derive(Args)]
pub struct PerfRecordArgs {
#[arg(long)]
pub reload: bool,
#[arg(long)]
pub duration: Option<u64>,
#[arg(long)]
pub file: Option<PathBuf>,
}
#[derive(Args)]
pub struct PerfAnalyzeArgs {
pub insight: String,
#[arg(long)]
pub trace_file: PathBuf,
}
#[derive(Args)]
pub struct PerfVitalsArgs {
#[arg(long)]
pub file: Option<PathBuf>,
}
#[derive(Args)]
pub struct JsArgs {
#[arg(long)]
pub frame: Option<String>,
#[command(subcommand)]
pub command: JsCommand,
}
#[derive(Subcommand)]
pub enum JsCommand {
#[command(
long_about = "Execute a JavaScript expression or script in the page context and return \
the result as JSON. Code can be provided as an inline argument, via --code (recommended \
for cross-platform quoting), read from a file with --file, or piped via stdin using \
'--stdin' or '-'. When --uid is specified, the code is wrapped in a function that \
receives the element as its first argument. By default, promise results are awaited; \
use --no-await to return immediately.",
after_long_help = "\
EXAMPLES:
# Evaluate an expression
agentchrome js exec \"document.title\"
# Use --code for cross-platform quoting (recommended on Windows)
agentchrome js exec --code \"document.querySelector('div')\"
# Execute a script file
agentchrome js exec --file script.js
# Run code on a specific element
agentchrome js exec --uid s3 \"(el) => el.textContent\"
# Read from stdin
echo 'document.URL' | agentchrome js exec --stdin
# Legacy stdin syntax (also works)
echo 'document.URL' | agentchrome js exec -
# Skip awaiting promises
agentchrome js exec --no-await \"fetch('/api/data')\""
)]
Exec(JsExecArgs),
}
#[derive(Args)]
pub struct JsExecArgs {
#[arg(conflicts_with_all = ["file", "code_flag", "stdin"])]
pub code: Option<String>,
#[arg(long = "code", id = "code_flag", conflicts_with_all = ["code", "file", "stdin"])]
pub code_flag: Option<String>,
#[arg(long, conflicts_with_all = ["code", "code_flag", "file"])]
pub stdin: bool,
#[arg(long, conflicts_with_all = ["code", "code_flag", "stdin"])]
pub file: Option<PathBuf>,
#[arg(long)]
pub uid: Option<String>,
#[arg(long, action = ArgAction::SetTrue)]
pub no_await: bool,
#[arg(long)]
pub timeout: Option<u64>,
#[arg(long)]
pub max_size: Option<usize>,
#[arg(long)]
pub worker: Option<u32>,
}
#[derive(Args)]
pub struct CookieArgs {
#[command(subcommand)]
pub command: CookieCommand,
}
#[derive(Subcommand)]
pub enum CookieCommand {
#[command(
long_about = "List cookies associated with the current page. By default, returns cookies \
scoped to the current page's URLs. Use --all to list all browser cookies regardless \
of URL. Use --domain to filter by a specific domain.",
after_long_help = "\
EXAMPLES:
# List cookies for the current page
agentchrome cookie list
# List all cookies
agentchrome cookie list --all
# Filter by domain
agentchrome cookie list --domain example.com"
)]
List(CookieListArgs),
#[command(
long_about = "Set a browser cookie with the given name and value. The --domain flag is \
strongly recommended to scope the cookie correctly. Additional flags control path, \
security attributes, SameSite policy, and expiry time.",
after_long_help = "\
EXAMPLES:
# Set a basic cookie
agentchrome cookie set session_id abc123 --domain example.com
# Set a secure, HttpOnly cookie
agentchrome cookie set token xyz --domain example.com --secure --http-only
# Set a cookie with SameSite and expiry
agentchrome cookie set prefs dark --domain example.com --same-site Lax --expires 1735689600"
)]
Set(CookieSetArgs),
#[command(
long_about = "Delete a cookie by name. Use --domain to scope the deletion to a specific \
domain when multiple cookies share the same name across different domains.",
after_long_help = "\
EXAMPLES:
# Delete a cookie by name
agentchrome cookie delete session_id
# Delete a cookie scoped to a specific domain
agentchrome cookie delete session_id --domain example.com"
)]
Delete(CookieDeleteArgs),
#[command(
long_about = "Remove all browser cookies. Returns the number of cookies that were cleared.",
after_long_help = "\
EXAMPLES:
# Clear all cookies
agentchrome cookie clear"
)]
Clear,
}
#[derive(Args)]
pub struct CookieListArgs {
#[arg(long)]
pub domain: Option<String>,
#[arg(long)]
pub all: bool,
}
#[derive(Args)]
pub struct CookieSetArgs {
pub name: String,
pub value: String,
#[arg(long)]
pub domain: Option<String>,
#[arg(long, default_value = "/")]
pub path: String,
#[arg(long)]
pub secure: bool,
#[arg(long)]
pub http_only: bool,
#[arg(long, value_name = "POLICY")]
pub same_site: Option<String>,
#[arg(long)]
pub expires: Option<f64>,
#[arg(long, hide = true, conflicts_with = "domain", value_name = "URL")]
pub url: Option<String>,
}
#[derive(Args)]
pub struct CookieDeleteArgs {
pub name: String,
#[arg(long)]
pub domain: Option<String>,
}
#[derive(Args)]
pub struct DialogArgs {
#[command(subcommand)]
pub command: DialogCommand,
}
#[derive(Subcommand)]
pub enum DialogCommand {
#[command(
long_about = "Accept or dismiss the currently open browser dialog (alert, confirm, \
prompt, or beforeunload). A dialog must be open before this command can be used. \
For prompt dialogs, use --text to provide the response text when accepting.",
after_long_help = "\
EXAMPLES:
# Accept an alert
agentchrome dialog handle accept
# Dismiss a confirm dialog
agentchrome dialog handle dismiss
# Accept a prompt with text
agentchrome dialog handle accept --text \"my response\""
)]
Handle(DialogHandleArgs),
#[command(
long_about = "Check whether a JavaScript dialog (alert, confirm, prompt, or \
beforeunload) is currently open. Returns JSON with the dialog's type, message, \
and default prompt text if applicable. Returns {\"open\": false} when no dialog \
is present.",
after_long_help = "\
EXAMPLES:
# Check for open dialog
agentchrome dialog info"
)]
Info,
}
#[derive(Args)]
pub struct DialogHandleArgs {
pub action: DialogAction,
#[arg(long)]
pub text: Option<String>,
}
#[derive(Debug, Clone, Copy, ValueEnum)]
pub enum DialogAction {
Accept,
Dismiss,
}
#[derive(Args)]
pub struct MediaArgs {
#[arg(long)]
pub frame: Option<String>,
#[command(subcommand)]
pub command: MediaCommand,
}
#[derive(Subcommand)]
pub enum MediaCommand {
#[command(
long_about = "Enumerate all HTML5 <audio> and <video> elements on the page and return \
their playback state. Each element includes its index (for targeting), tag type, \
source URLs, duration, current time, playback state, mute/volume status, and \
readyState. Returns an empty array if no media elements exist.",
after_long_help = "\
EXAMPLES:
# List all media elements
agentchrome media list
# List media in a specific iframe
agentchrome media --frame 0 list"
)]
List,
#[command(
long_about = "Start playback of a media element identified by index (from 'media list') \
or CSS selector (prefixed with 'css:'). Returns the updated playback state as JSON. \
Use --all to play all media elements on the page.",
after_long_help = "\
EXAMPLES:
# Play by index
agentchrome media play 0
# Play by CSS selector
agentchrome media play css:audio.narration
# Play all media elements
agentchrome media play --all"
)]
Play(MediaTargetArgs),
#[command(
long_about = "Pause playback of a media element identified by index or CSS selector. \
Returns the updated playback state as JSON. Use --all to pause all media elements.",
after_long_help = "\
EXAMPLES:
# Pause by index
agentchrome media pause 0
# Pause all media elements
agentchrome media pause --all"
)]
Pause(MediaTargetArgs),
#[command(
long_about = "Set the current playback position of a media element to a specific time \
in seconds. The time is clamped to the element's duration by the browser. Returns \
the updated playback state as JSON. Use --all with --time to seek all elements.",
after_long_help = "\
EXAMPLES:
# Seek to 15.5 seconds
agentchrome media seek 0 15.5
# Seek all elements to 10 seconds
agentchrome media seek --all --time 10.0"
)]
Seek(MediaSeekArgs),
#[command(
long_about = "Set the current playback position of a media element to its total duration, \
effectively ending playback. This is the primary use case for skipping audio narration \
gates in SCORM courses. Returns the updated playback state as JSON. Use --all to seek \
all media elements to their end.",
after_long_help = "\
EXAMPLES:
# Seek a specific element to end
agentchrome media seek-end 0
# Seek all media elements to end
agentchrome media seek-end --all"
)]
SeekEnd(MediaTargetArgs),
}
#[derive(Args)]
pub struct MediaTargetArgs {
pub target: Option<String>,
#[arg(long, conflicts_with = "target")]
pub all: bool,
}
#[derive(Args)]
pub struct MediaSeekArgs {
pub target: Option<String>,
#[arg(conflicts_with = "time")]
pub time_pos: Option<f64>,
#[arg(long, conflicts_with = "target")]
pub all: bool,
#[arg(long, conflicts_with = "time_pos")]
pub time: Option<f64>,
}
#[derive(Args)]
pub struct AuditArgs {
#[command(subcommand)]
pub command: AuditCommand,
}
#[derive(Subcommand)]
pub enum AuditCommand {
#[command(
long_about = "Run a Google Lighthouse audit against the current page (or a given URL). \
Connects Lighthouse to the managed Chrome session via the CDP port and returns \
structured JSON category scores on stdout. Use --only to limit which categories \
are measured. Use --output-file to save the full Lighthouse JSON report.",
after_long_help = "\
PREREQUISITES:
Requires the lighthouse npm package. Install with:
npm install -g lighthouse
Or run:
agentchrome audit lighthouse --install-prereqs
EXAMPLES:
# Full audit on the current page
agentchrome audit lighthouse
# Audit a specific URL
agentchrome audit lighthouse https://example.com
# Only performance and accessibility
agentchrome audit lighthouse --only performance,accessibility
# Save the full report
agentchrome audit lighthouse --output-file report.json
# Install the lighthouse prerequisite (runs: npm install -g lighthouse)
agentchrome audit lighthouse --install-prereqs"
)]
Lighthouse(AuditLighthouseArgs),
}
#[derive(Args)]
pub struct AuditLighthouseArgs {
pub url: Option<String>,
#[arg(long)]
pub only: Option<String>,
#[arg(long)]
pub output_file: Option<PathBuf>,
#[arg(long)]
pub install_prereqs: bool,
}
#[derive(Args)]
#[command(group(clap::ArgGroup::new("target").required(true).args(["url", "current"])))]
pub struct DiagnoseArgs {
#[arg(conflicts_with = "current")]
pub url: Option<String>,
#[arg(long, conflicts_with = "url")]
pub current: bool,
#[arg(long, value_enum, default_value_t = WaitUntil::Load)]
pub wait_until: WaitUntil,
#[arg(long)]
pub timeout: Option<u64>,
}
#[allow(clippy::struct_excessive_bools)]
#[derive(Args)]
pub struct MarkdownArgs {
#[arg(long, conflicts_with_all = ["stdin", "url"])]
pub file: Option<PathBuf>,
#[arg(long, conflicts_with_all = ["file", "url"])]
pub stdin: bool,
#[arg(long, value_name = "URL", conflicts_with_all = ["file", "stdin"])]
pub url: Option<String>,
#[arg(long, value_name = "URL")]
pub base_url: Option<String>,
#[arg(long, value_name = "CSS")]
pub selector: Option<String>,
#[arg(long)]
pub strip_links: bool,
#[arg(long)]
pub include_images: bool,
#[arg(
long,
value_name = "BYTES",
value_parser = parse_nonzero_usize,
default_value_t = DEFAULT_MARKDOWN_MAX_INPUT_BYTES
)]
pub max_input_bytes: usize,
}
#[derive(Args)]
pub struct SkillArgs {
#[command(subcommand)]
pub command: SkillCommand,
}
#[derive(Subcommand)]
pub enum SkillCommand {
#[command(
long_about = "Install a concise agentchrome skill/instruction file for the detected (or \
specified) agentic coding tool. Without --tool, installs into every detected \
supported agent target. With --tool, installs only into that explicit target. \
The skill tells the AI agent what agentchrome is and how to discover its \
capabilities. Re-running install overwrites the existing skill file (idempotent).",
after_long_help = "\
EXAMPLES:
# Install into every detected supported agent
agentchrome skill install
# Install for a specific tool
agentchrome skill install --tool claude-code
# Install for Codex
agentchrome skill install --tool codex"
)]
Install(SkillInstallArgs),
#[command(
long_about = "Remove a previously installed agentchrome skill file for the detected (or \
specified) agentic coding tool. For tools with shared rule files, only the \
agentchrome section is removed.",
after_long_help = "\
EXAMPLES:
# Auto-detect and uninstall
agentchrome skill uninstall
# Uninstall for a specific tool
agentchrome skill uninstall --tool cursor"
)]
Uninstall(SkillToolArgs),
#[command(
long_about = "Replace an installed agentchrome skill file with the current version's \
content. Without --tool, refreshes every installed AgentChrome skill whose embedded \
version is older than the running binary. With --tool, updates only that explicit \
target and errors if no skill is currently installed for that tool.",
after_long_help = "\
EXAMPLES:
# Update every stale installed AgentChrome skill
agentchrome skill update
# Update for a specific tool
agentchrome skill update --tool claude-code"
)]
Update(SkillToolArgs),
#[command(
long_about = "List all supported agentic coding tools with their detection method, \
install path, and whether a skill is currently installed.",
after_long_help = "\
EXAMPLES:
# List all tools
agentchrome skill list"
)]
List,
}
#[derive(Args)]
pub struct SkillInstallArgs {
#[arg(long, value_enum)]
pub tool: Option<ToolName>,
}
#[derive(Args)]
pub struct SkillToolArgs {
#[arg(long, value_enum)]
pub tool: Option<ToolName>,
}
#[derive(Debug, Clone, ValueEnum)]
pub enum ToolName {
ClaudeCode,
Windsurf,
Aider,
Continue,
CopilotJb,
Cursor,
Gemini,
Codex,
}
#[derive(Debug, Clone, Copy, ValueEnum)]
pub enum MouseButton {
Left,
Middle,
Right,
}
#[derive(Debug, Clone, Copy, ValueEnum, Default, PartialEq, Eq)]
pub enum WaitUntil {
#[default]
Load,
Domcontentloaded,
Networkidle,
None,
}
#[allow(clippy::struct_excessive_bools)]
#[derive(Args)]
pub struct ConnectArgs {
#[arg(long)]
pub launch: bool,
#[arg(long, conflicts_with_all = ["launch", "disconnect"])]
pub status: bool,
#[arg(long, conflicts_with_all = ["launch", "status"])]
pub disconnect: bool,
#[arg(long, requires = "launch")]
pub headless: bool,
#[arg(long, requires = "launch", default_value = "stable")]
pub channel: ChromeChannel,
#[arg(long, requires = "launch")]
pub chrome_path: Option<PathBuf>,
#[arg(long, requires = "launch")]
pub chrome_arg: Vec<String>,
}
#[derive(Args)]
pub struct InteractArgs {
#[arg(long)]
pub frame: Option<String>,
#[command(subcommand)]
pub command: InteractCommand,
}
#[derive(Subcommand)]
pub enum InteractCommand {
#[command(
long_about = "Click an element identified by UID (from 'page snapshot', e.g., 's5') or \
CSS selector (prefixed with 'css:', e.g., 'css:#submit'). By default, performs a \
left single-click at the element's center. Use --double for double-click or --right \
for right-click (context menu). These flags are mutually exclusive.",
after_long_help = "\
EXAMPLES:
# Click by UID
agentchrome interact click s5
# Click by CSS selector
agentchrome interact click css:#submit-btn
# Double-click
agentchrome interact click s5 --double
# Right-click (context menu)
agentchrome interact click s5 --right"
)]
Click(ClickArgs),
#[command(
long_about = "Click at specific viewport coordinates (X, Y in pixels). Useful when \
targeting elements that are not in the accessibility tree or for precise coordinate-\
based interactions. Use --double for double-click or --right for right-click.",
after_long_help = "\
EXAMPLES:
# Click at coordinates
agentchrome interact click-at 100 200
# Double-click at coordinates
agentchrome interact click-at 100 200 --double"
)]
ClickAt(ClickAtArgs),
#[command(
long_about = "Move the mouse over an element identified by UID or CSS selector. \
Triggers hover effects, tooltips, and mouseover events. Does not click.",
after_long_help = "\
EXAMPLES:
# Hover by UID
agentchrome interact hover s3
# Hover by CSS selector
agentchrome interact hover css:.tooltip-trigger"
)]
Hover(HoverArgs),
#[command(
long_about = "Drag from one element to another. Both source and target are identified \
by UID or CSS selector. Simulates mouse down on the source, move to the target, \
and mouse up on the target.",
after_long_help = "\
EXAMPLES:
# Drag between elements by UID
agentchrome interact drag s3 s7
# Drag using CSS selectors
agentchrome interact drag css:#item css:#dropzone"
)]
Drag(DragArgs),
#[command(
long_about = "Drag from one set of viewport coordinates to another. Simulates mouse down \
at the source coordinates, move to the target coordinates, and mouse up at the target. \
Use --steps to interpolate intermediate mousemove events for applications that track \
drag movement (e.g., canvas-based interfaces).",
after_long_help = "\
EXAMPLES:
# Drag from (100,200) to (300,400)
agentchrome interact drag-at 100 200 300 400
# Drag with interpolated steps
agentchrome interact drag-at 0 0 500 500 --steps 10
# Drag inside an iframe
agentchrome interact --frame 1 drag-at 50 60 200 300"
)]
DragAt(DragAtArgs),
#[command(
name = "mousedown-at",
long_about = "Dispatch only a mousePressed event at specific viewport coordinates. \
No mouseReleased event is sent, allowing decomposed mouse interactions such as \
long-press, drag sequences across multiple invocations, or custom interaction \
patterns. Use --button to specify left, middle, or right mouse button.",
after_long_help = "\
EXAMPLES:
# Mousedown at coordinates
agentchrome interact mousedown-at 100 200
# Right-button mousedown
agentchrome interact mousedown-at 100 200 --button right
# Mousedown inside an iframe
agentchrome interact --frame 1 mousedown-at 50 60"
)]
MouseDownAt(MouseDownAtArgs),
#[command(
name = "mouseup-at",
long_about = "Dispatch only a mouseReleased event at specific viewport coordinates. \
No mousePressed event is sent, allowing decomposed mouse interactions such as \
completing a drag started by a prior mousedown-at invocation. \
Use --button to specify left, middle, or right mouse button.",
after_long_help = "\
EXAMPLES:
# Mouseup at coordinates
agentchrome interact mouseup-at 300 400
# Right-button mouseup
agentchrome interact mouseup-at 300 400 --button right
# Mouseup inside an iframe
agentchrome interact --frame 1 mouseup-at 50 60"
)]
MouseUpAt(MouseUpAtArgs),
#[command(
long_about = "Type text character-by-character into the currently focused element. \
Simulates individual key press and release events for each character. Use --delay \
to add a pause between keystrokes. To focus an element first, use 'interact click'.",
after_long_help = "\
EXAMPLES:
# Type text
agentchrome interact type \"Hello, world!\"
# Type with delay between keystrokes
agentchrome interact type \"slow typing\" --delay 50"
)]
Type(TypeArgs),
#[command(
long_about = "Press a key or key combination. Supports modifier keys (Control, Shift, \
Alt, Meta) combined with regular keys using '+' separator. Use --repeat to press \
the key multiple times. Common keys: Enter, Tab, Escape, Backspace, ArrowUp, \
ArrowDown, ArrowLeft, ArrowRight, Home, End, PageUp, PageDown, Delete.",
after_long_help = "\
EXAMPLES:
# Press Enter
agentchrome interact key Enter
# Select all (Ctrl+A)
agentchrome interact key Control+A
# Press Tab 3 times
agentchrome interact key Tab --repeat 3
# Multi-modifier combo
agentchrome interact key Control+Shift+ArrowRight"
)]
Key(KeyArgs),
#[command(
long_about = "Scroll the page or a specific container element. By default, scrolls \
down by one viewport height. Use --direction to scroll in other directions, \
--amount to set a custom distance in pixels, or the shortcut flags --to-top, \
--to-bottom, --to-element to scroll to specific positions. Use --selector or \
--uid to scroll within a specific scrollable container by CSS selector or \
accessibility UID. Use --container for the legacy combined-target syntax. \
Use --smooth for animated scrolling.",
after_long_help = "\
EXAMPLES:
# Scroll down one viewport height
agentchrome interact scroll
# Scroll up 200 pixels
agentchrome interact scroll --direction up --amount 200
# Scroll to bottom of page
agentchrome interact scroll --to-bottom
# Scroll until an element is visible
agentchrome interact scroll --to-element s15
# Scroll a container by CSS selector
agentchrome interact scroll --selector \".stage\" --direction down
# Scroll a container by UID (requires prior snapshot)
agentchrome interact scroll --uid s42 --direction down --amount 300
# Smooth scroll within a container
agentchrome interact scroll --container css:.scrollable --smooth"
)]
Scroll(ScrollArgs),
}
#[allow(clippy::struct_excessive_bools)]
#[derive(Args)]
pub struct ClickArgs {
pub target: String,
#[arg(long, conflicts_with = "right")]
pub double: bool,
#[arg(long, conflicts_with = "double")]
pub right: bool,
#[arg(long)]
pub include_snapshot: bool,
#[arg(long)]
pub compact: bool,
#[arg(long, value_enum)]
pub wait_until: Option<WaitUntil>,
}
#[allow(clippy::struct_excessive_bools)]
#[derive(Args)]
pub struct ClickAtArgs {
#[arg(value_parser = clap::value_parser!(CoordValue), allow_hyphen_values = true)]
pub x: CoordValue,
#[arg(value_parser = clap::value_parser!(CoordValue), allow_hyphen_values = true)]
pub y: CoordValue,
#[arg(long = "relative-to")]
pub relative_to: Option<String>,
#[arg(long, conflicts_with = "right")]
pub double: bool,
#[arg(long, conflicts_with = "double")]
pub right: bool,
#[arg(long)]
pub include_snapshot: bool,
#[arg(long)]
pub compact: bool,
#[arg(long, value_enum)]
pub wait_until: Option<WaitUntil>,
}
#[derive(Args)]
pub struct HoverArgs {
pub target: String,
#[arg(long)]
pub include_snapshot: bool,
#[arg(long)]
pub compact: bool,
}
#[derive(Args)]
pub struct DragArgs {
pub from: String,
pub to: String,
#[arg(long)]
pub include_snapshot: bool,
#[arg(long)]
pub compact: bool,
}
#[derive(Args)]
pub struct DragAtArgs {
#[arg(value_parser = clap::value_parser!(CoordValue), allow_hyphen_values = true)]
pub from_x: CoordValue,
#[arg(value_parser = clap::value_parser!(CoordValue), allow_hyphen_values = true)]
pub from_y: CoordValue,
#[arg(value_parser = clap::value_parser!(CoordValue), allow_hyphen_values = true)]
pub to_x: CoordValue,
#[arg(value_parser = clap::value_parser!(CoordValue), allow_hyphen_values = true)]
pub to_y: CoordValue,
#[arg(long = "relative-to")]
pub relative_to: Option<String>,
#[arg(long)]
pub steps: Option<u32>,
#[arg(long)]
pub include_snapshot: bool,
#[arg(long)]
pub compact: bool,
}
#[derive(Args)]
pub struct MouseDownAtArgs {
#[arg(value_parser = clap::value_parser!(CoordValue), allow_hyphen_values = true)]
pub x: CoordValue,
#[arg(value_parser = clap::value_parser!(CoordValue), allow_hyphen_values = true)]
pub y: CoordValue,
#[arg(long = "relative-to")]
pub relative_to: Option<String>,
#[arg(long, value_enum)]
pub button: Option<MouseButton>,
#[arg(long)]
pub include_snapshot: bool,
#[arg(long)]
pub compact: bool,
}
#[derive(Args)]
pub struct MouseUpAtArgs {
#[arg(value_parser = clap::value_parser!(CoordValue), allow_hyphen_values = true)]
pub x: CoordValue,
#[arg(value_parser = clap::value_parser!(CoordValue), allow_hyphen_values = true)]
pub y: CoordValue,
#[arg(long = "relative-to")]
pub relative_to: Option<String>,
#[arg(long, value_enum)]
pub button: Option<MouseButton>,
#[arg(long)]
pub include_snapshot: bool,
#[arg(long)]
pub compact: bool,
}
#[derive(Args)]
pub struct TypeArgs {
#[arg(required = true)]
pub text: String,
#[arg(long, default_value_t = 0)]
pub delay: u64,
#[arg(long)]
pub include_snapshot: bool,
#[arg(long)]
pub compact: bool,
}
#[derive(Args)]
pub struct KeyArgs {
#[arg(required = true)]
pub keys: String,
#[arg(long, default_value_t = 1)]
pub repeat: u32,
#[arg(long)]
pub include_snapshot: bool,
#[arg(long)]
pub compact: bool,
}
#[derive(Debug, Clone, Copy, ValueEnum, Default)]
pub enum ScrollDirection {
#[default]
Down,
Up,
Left,
Right,
}
#[allow(clippy::struct_excessive_bools)]
#[derive(Args)]
pub struct ScrollArgs {
#[arg(long, value_enum, default_value_t = ScrollDirection::Down,
conflicts_with_all = ["to_element", "to_top", "to_bottom"])]
pub direction: ScrollDirection,
#[arg(long, conflicts_with_all = ["to_element", "to_top", "to_bottom"])]
pub amount: Option<u32>,
#[arg(long, conflicts_with_all = ["direction", "amount", "to_top", "to_bottom", "container"])]
pub to_element: Option<String>,
#[arg(long, conflicts_with_all = ["direction", "amount", "to_element", "to_bottom", "container"])]
pub to_top: bool,
#[arg(long, conflicts_with_all = ["direction", "amount", "to_element", "to_top", "container"])]
pub to_bottom: bool,
#[arg(long)]
pub smooth: bool,
#[arg(long, conflicts_with_all = ["uid", "to_element", "to_top", "to_bottom", "container"])]
pub selector: Option<String>,
#[arg(long, conflicts_with_all = ["selector", "to_element", "to_top", "to_bottom", "container"])]
pub uid: Option<String>,
#[arg(long, conflicts_with_all = ["to_element", "to_top", "to_bottom", "selector", "uid"])]
pub container: Option<String>,
#[arg(long)]
pub include_snapshot: bool,
#[arg(long)]
pub compact: bool,
}
#[derive(Args)]
pub struct FormArgs {
#[arg(long)]
pub frame: Option<String>,
#[command(subcommand)]
pub command: FormCommand,
}
#[derive(Subcommand)]
pub enum FormCommand {
#[command(
long_about = "Set the value of a form field identified by UID (from 'page snapshot', \
e.g., 's5') or CSS selector (prefixed with 'css:', e.g., 'css:#email'). Works \
with text inputs, textareas, select dropdowns, checkboxes, and ARIA combobox \
elements (role=\"combobox\"). Combobox elements are automatically detected and \
filled using a click-type-confirm sequence. Dispatches change and input events \
to trigger form validation.",
after_long_help = "\
EXAMPLES:
# Fill by UID
agentchrome form fill s5 \"hello@example.com\"
# Fill by CSS selector
agentchrome form fill css:#email \"user@example.com\"
# Select a dropdown option
agentchrome form fill s8 \"Option B\"
# Fill an ARIA combobox
agentchrome form fill s5 \"Acme Corp\"
# Custom confirmation key for combobox
agentchrome form fill --confirm-key Tab s5 \"Acme Corp\""
)]
Fill(FormFillArgs),
#[command(
long_about = "Fill multiple form fields in a single command. Accepts a JSON array of \
{target, value} objects either as an inline argument or from a file with --file. Each \
target is a UID (like 's5') or a CSS selector (prefixed with 'css:'), matching the \
vocabulary of `form fill`. Each field is filled in order. Useful for completing \
entire forms in one step. The legacy `uid` key is still accepted as an alias for \
`target` so existing scripts keep working.",
after_long_help = "\
EXAMPLES:
# Fill multiple fields inline
agentchrome form fill-many '[{\"target\":\"s5\",\"value\":\"Alice\"},{\"target\":\"s7\",\"value\":\"alice@example.com\"}]'
# Fill from a JSON file
agentchrome form fill-many --file form-data.json"
)]
FillMany(FormFillManyArgs),
#[command(
long_about = "Clear the value of a form field identified by UID or CSS selector. \
Sets the field to an empty string and dispatches change and input events.",
after_long_help = "\
EXAMPLES:
# Clear a field by UID
agentchrome form clear s5
# Clear by CSS selector
agentchrome form clear css:#search-input"
)]
Clear(FormClearArgs),
#[command(
long_about = "Upload one or more files to a file input element identified by UID or \
CSS selector. The element must be an <input type=\"file\">. Multiple file paths \
can be specified for multi-file upload inputs.",
after_long_help = "\
EXAMPLES:
# Upload a single file
agentchrome form upload s10 ./photo.jpg
# Upload multiple files
agentchrome form upload css:#file-input ./doc1.pdf ./doc2.pdf"
)]
Upload(FormUploadArgs),
#[command(
long_about = "Submit a form identified by UID (from 'page snapshot', e.g., 's3') or \
CSS selector (prefixed with 'css:', e.g., 'css:#login-form'). The target can be \
the form element itself or any element inside the form — the parent form is \
resolved automatically. Uses requestSubmit() to respect browser validation.",
after_long_help = "\
EXAMPLES:
# Submit by form UID
agentchrome form submit s3
# Submit by CSS selector
agentchrome form submit css:#login-form
# Submit targeting an input inside the form
agentchrome form submit s5
# Include updated snapshot after submit
agentchrome form submit s3 --include-snapshot"
)]
Submit(FormSubmitArgs),
}
#[derive(Args)]
pub struct FormFillArgs {
pub target: String,
pub value: String,
#[arg(long)]
pub confirm_key: Option<String>,
#[arg(long)]
pub include_snapshot: bool,
#[arg(long)]
pub compact: bool,
}
#[derive(Args)]
pub struct FormFillManyArgs {
#[arg(value_name = "JSON")]
pub input: Option<String>,
#[arg(long)]
pub file: Option<PathBuf>,
#[arg(long)]
pub include_snapshot: bool,
#[arg(long)]
pub compact: bool,
}
#[derive(Args)]
pub struct FormClearArgs {
pub target: String,
#[arg(long)]
pub include_snapshot: bool,
#[arg(long)]
pub compact: bool,
}
#[derive(Args)]
pub struct FormUploadArgs {
pub target: String,
#[arg(required = true)]
pub files: Vec<PathBuf>,
#[arg(long)]
pub include_snapshot: bool,
#[arg(long)]
pub compact: bool,
}
#[derive(Args)]
pub struct FormSubmitArgs {
#[arg(value_name = "TARGET")]
pub target: String,
#[arg(long)]
pub include_snapshot: bool,
#[arg(long)]
pub compact: bool,
}
#[derive(Args)]
pub struct ConsoleArgs {
#[command(subcommand)]
pub command: ConsoleCommand,
}
#[derive(Subcommand)]
pub enum ConsoleCommand {
#[command(
long_about = "Read captured console messages from the current page. Without arguments, \
lists recent messages with their IDs, types, and text. Pass a message ID to get \
full details including stack trace and arguments. Filter by type or use --errors-only \
for error and assert messages only.",
after_long_help = "\
EXAMPLES:
# List recent console messages
agentchrome console read
# Get details of a specific message
agentchrome console read 42
# Show only errors
agentchrome console read --errors-only
# Filter by type
agentchrome console read --type warn,error --limit 20"
)]
Read(ConsoleReadArgs),
#[command(
long_about = "Stream new console messages in real time as they are logged, similar to \
'tail -f'. Each message is printed as a JSON line. Use --timeout to auto-exit \
after a specified duration. Filter by type or use --errors-only to stream only \
error and assert messages.\n\n\
By default, the command monitors output and exits 0 when the timeout elapses \
or Ctrl+C is pressed, regardless of log levels observed. Pass --fail-on-error \
to turn the stream into a CI assertion: if any error-level message is observed \
during the window, the command exits 1 with a JSON error on stderr \
({\"error\":\"Error-level console messages were seen\",\"code\":1}).",
after_long_help = "\
EXAMPLES:
# Stream all console output (exit 0 on timeout / Ctrl+C)
agentchrome console follow
# Stream errors only for 10 seconds (monitoring — exit 0 even if errors are seen)
agentchrome console follow --errors-only --timeout 10000
# Stream specific message types
agentchrome console follow --type log,warn
# CI assertion — exit 1 if any console.error is observed during the window
agentchrome console follow --timeout 10000 --fail-on-error"
)]
Follow(ConsoleFollowArgs),
}
#[derive(Args)]
pub struct ConsoleReadArgs {
pub msg_id: Option<u64>,
#[arg(long, value_name = "TYPES", conflicts_with = "errors_only")]
pub r#type: Option<String>,
#[arg(long, conflicts_with = "type")]
pub errors_only: bool,
#[arg(long, default_value_t = 50)]
pub limit: usize,
#[arg(long, default_value_t = 0)]
pub page: usize,
#[arg(long)]
pub include_preserved: bool,
}
#[derive(Args)]
pub struct ConsoleFollowArgs {
#[arg(long, value_name = "TYPES", conflicts_with = "errors_only")]
pub r#type: Option<String>,
#[arg(long, conflicts_with = "type")]
pub errors_only: bool,
#[arg(long)]
pub timeout: Option<u64>,
#[arg(long)]
pub fail_on_error: bool,
}
#[derive(Args)]
pub struct NetworkArgs {
#[command(subcommand)]
pub command: NetworkCommand,
}
#[derive(Subcommand)]
pub enum NetworkCommand {
#[command(
long_about = "List captured network requests from the current page. Returns JSON with \
each request's ID, method, URL, status, resource type, and timing. Filter by \
resource type, URL pattern, HTTP status code, or HTTP method. Use --limit and \
--page for pagination.",
after_long_help = "\
EXAMPLES:
# List recent requests
agentchrome network list
# Filter by resource type
agentchrome network list --type xhr,fetch
# Filter by URL pattern
agentchrome network list --url api.example.com
# Filter by status code
agentchrome network list --status 4xx"
)]
List(NetworkListArgs),
#[command(
long_about = "Get detailed information about a specific network request by its numeric \
ID. Returns JSON with full request and response headers, timing breakdown, and \
body size. Use --save-request or --save-response to save the request or response \
body to a file.",
after_long_help = "\
EXAMPLES:
# Get request details
agentchrome network get 42
# Save the response body to a file
agentchrome network get 42 --save-response body.json
# Save both request and response bodies
agentchrome network get 42 --save-request req.json --save-response resp.json"
)]
Get(NetworkGetArgs),
#[command(
long_about = "Stream network requests in real time as they are made, similar to \
'tail -f'. Each request is printed as a JSON line. Filter by resource type, \
URL pattern, or HTTP method. Use --timeout to auto-exit after a specified \
duration. Use --verbose to include request and response headers.",
after_long_help = "\
EXAMPLES:
# Stream all network requests
agentchrome network follow
# Stream API requests only
agentchrome network follow --type xhr,fetch --url /api/
# Stream with headers for 30 seconds
agentchrome network follow --verbose --timeout 30000"
)]
Follow(NetworkFollowArgs),
}
#[derive(Args)]
pub struct NetworkListArgs {
#[arg(long, value_name = "TYPES")]
pub r#type: Option<String>,
#[arg(long)]
pub url: Option<String>,
#[arg(long)]
pub status: Option<String>,
#[arg(long)]
pub method: Option<String>,
#[arg(long, default_value_t = 50)]
pub limit: usize,
#[arg(long, default_value_t = 0)]
pub page: usize,
#[arg(long)]
pub include_preserved: bool,
#[arg(long)]
pub frame: Option<String>,
}
#[derive(Args)]
pub struct NetworkGetArgs {
pub req_id: u64,
#[arg(long)]
pub save_request: Option<PathBuf>,
#[arg(long)]
pub save_response: Option<PathBuf>,
}
#[derive(Args)]
pub struct NetworkFollowArgs {
#[arg(long, value_name = "TYPES")]
pub r#type: Option<String>,
#[arg(long)]
pub url: Option<String>,
#[arg(long)]
pub method: Option<String>,
#[arg(long)]
pub timeout: Option<u64>,
#[arg(long)]
pub verbose: bool,
}
#[derive(Args)]
pub struct PageResizeArgs {
pub size: String,
}
#[derive(Args)]
pub struct PageElementArgs {
pub target: String,
}
#[derive(Args)]
pub struct PageHitTestArgs {
pub x: u32,
pub y: u32,
}
#[derive(Args)]
pub struct PageCoordsArgs {
#[arg(long)]
pub selector: String,
}
#[derive(Args)]
pub struct PageWaitArgs {
#[arg(long, group = "condition")]
pub url: Option<String>,
#[arg(long, group = "condition")]
pub text: Option<String>,
#[arg(long, group = "condition")]
pub selector: Option<String>,
#[arg(long, group = "condition")]
pub network_idle: bool,
#[arg(long, group = "condition")]
pub js_expression: Option<String>,
#[arg(long, requires = "selector", default_value = "1")]
pub count: u64,
#[arg(long, default_value = "100")]
pub interval: u64,
}
#[derive(Args)]
pub struct DomArgs {
#[arg(long)]
pub frame: Option<String>,
#[arg(long)]
pub pierce_shadow: bool,
#[command(subcommand)]
pub command: DomCommand,
}
#[derive(Subcommand)]
pub enum DomCommand {
#[command(
alias = "query",
long_about = "Query elements in the DOM by CSS selector (default) or XPath expression \
(with --xpath). Returns a JSON array of matching elements with their node IDs, \
tag names, attributes, and text content. Node IDs can be used with other dom \
subcommands.",
after_long_help = "\
EXAMPLES:
# Select by CSS selector
agentchrome dom select \"h1\"
# Select by XPath
agentchrome dom select \"//a[@href]\" --xpath
# Select with a complex CSS selector
agentchrome dom select \"div.content > p:first-child\""
)]
Select(DomSelectArgs),
#[command(
name = "get-attribute",
long_about = "Read a single attribute value from a DOM element. The element can be \
targeted by node ID (from 'dom select'), snapshot UID (from 'page snapshot'), \
or CSS selector (prefixed with 'css:'). Returns the attribute name and value.",
after_long_help = "\
EXAMPLES:
# Get href by UID
agentchrome dom get-attribute s3 href
# Get class by CSS selector
agentchrome dom get-attribute css:h1 class"
)]
GetAttribute(DomGetAttributeArgs),
#[command(
name = "get-text",
long_about = "Read the textContent of a DOM element. Returns the combined text of \
the element and all its descendants.",
after_long_help = "\
EXAMPLES:
# Get text by UID
agentchrome dom get-text s3
# Get text by CSS selector
agentchrome dom get-text css:h1"
)]
GetText(DomNodeIdArgs),
#[command(
name = "get-html",
long_about = "Read the outerHTML of a DOM element, including the element itself and \
all its children as an HTML string.",
after_long_help = "\
EXAMPLES:
# Get HTML by UID
agentchrome dom get-html s3
# Get HTML by CSS selector
agentchrome dom get-html css:div.content"
)]
GetHtml(DomNodeIdArgs),
#[command(
name = "set-attribute",
long_about = "Set or update an attribute on a DOM element. Creates the attribute if \
it doesn't exist, or updates its value if it does.",
after_long_help = "\
EXAMPLES:
# Set class attribute
agentchrome dom set-attribute s5 class \"highlight\"
# Set data attribute by CSS selector
agentchrome dom set-attribute css:#main data-active true"
)]
SetAttribute(DomSetAttributeArgs),
#[command(
name = "set-text",
long_about = "Replace the textContent of a DOM element, removing all child nodes \
and setting the element's content to the given text.",
after_long_help = "\
EXAMPLES:
# Set text by UID
agentchrome dom set-text s3 \"New heading\"
# Set text by CSS selector
agentchrome dom set-text css:h1 \"Updated Title\""
)]
SetText(DomSetTextArgs),
#[command(
long_about = "Remove a DOM element and all its children from the document. This is \
irreversible within the current page session.",
after_long_help = "\
EXAMPLES:
# Remove by UID
agentchrome dom remove s3
# Remove by CSS selector
agentchrome dom remove css:div.ad-banner"
)]
Remove(DomNodeIdArgs),
#[command(
name = "get-style",
long_about = "Read the computed CSS styles of a DOM element. Without a property name, \
returns all computed styles. With a property name, returns just that property's \
value.",
after_long_help = "\
EXAMPLES:
# Get all computed styles
agentchrome dom get-style s3
# Get a specific property
agentchrome dom get-style s3 display
# Get style by CSS selector
agentchrome dom get-style css:h1 color"
)]
GetStyle(DomGetStyleArgs),
#[command(
name = "set-style",
long_about = "Set the inline style attribute of a DOM element. The style string replaces \
the entire inline style. Use CSS property syntax.",
after_long_help = "\
EXAMPLES:
# Set inline style
agentchrome dom set-style s3 \"color: red; font-size: 24px\"
# Set style by CSS selector
agentchrome dom set-style css:h1 \"display: none\""
)]
SetStyle(DomSetStyleArgs),
#[command(
long_about = "Navigate to the parent element of a DOM node. Returns the parent's \
node ID, tag name, attributes, and text content.",
after_long_help = "\
EXAMPLES:
# Get parent of a UID
agentchrome dom parent s3
# Get parent by CSS selector
agentchrome dom parent css:span.label"
)]
Parent(DomNodeIdArgs),
#[command(
long_about = "List the direct child elements (element nodes only, nodeType 1) of a \
DOM node. Returns a JSON array of child elements.",
after_long_help = "\
EXAMPLES:
# List children by UID
agentchrome dom children s3
# List children by CSS selector
agentchrome dom children css:div.container"
)]
Children(DomNodeIdArgs),
#[command(
long_about = "List the sibling elements of a DOM node (other children of the same \
parent, excluding the target element itself).",
after_long_help = "\
EXAMPLES:
# List siblings by UID
agentchrome dom siblings s3
# List siblings by CSS selector
agentchrome dom siblings css:li.active"
)]
Siblings(DomNodeIdArgs),
#[command(
long_about = "Display the DOM tree as indented plain text. By default shows the full \
document tree. Use --depth to limit traversal depth and --root to start from a \
specific element.",
after_long_help = "\
EXAMPLES:
# Show the full DOM tree
agentchrome dom tree
# Limit depth to 3 levels
agentchrome dom tree --depth 3
# Show tree from a specific element (positional form)
agentchrome dom tree css:div.content
# Show tree from a specific element (flag form)
agentchrome dom tree --root css:div.content"
)]
Tree(DomTreeArgs),
#[command(
long_about = "List all event listeners attached to a DOM element. Shows listeners \
registered via addEventListener and inline handlers (e.g., onclick). Output \
includes event type, capture/bubble phase, once/passive flags, and handler \
source location.",
after_long_help = "\
EXAMPLES:
# List listeners by UID
agentchrome dom events s3
# List listeners by CSS selector
agentchrome dom events css:button
# List listeners in a frame
agentchrome dom --frame 0 events css:button"
)]
Events(DomNodeIdArgs),
}
#[derive(Args)]
pub struct DomSelectArgs {
pub selector: String,
#[arg(long)]
pub xpath: bool,
}
#[derive(Args)]
pub struct DomGetAttributeArgs {
pub node_id: String,
pub attribute: String,
}
#[derive(Args)]
pub struct DomSetAttributeArgs {
pub node_id: String,
pub attribute: String,
pub value: String,
}
#[derive(Args)]
pub struct DomGetStyleArgs {
pub node_id: String,
pub property: Option<String>,
}
#[derive(Args)]
pub struct DomSetStyleArgs {
pub node_id: String,
pub style: String,
}
#[derive(Args)]
pub struct DomSetTextArgs {
pub node_id: String,
pub text: String,
}
#[derive(Args)]
pub struct DomNodeIdArgs {
pub node_id: String,
}
#[derive(Args)]
pub struct DomTreeArgs {
#[arg(long)]
pub depth: Option<u32>,
#[arg(long)]
pub root: Option<String>,
#[arg(value_name = "ROOT", conflicts_with = "root")]
pub root_positional: Option<String>,
}
#[derive(Args)]
pub struct EmulateArgs {
#[command(subcommand)]
pub command: EmulateCommand,
}
#[derive(Subcommand)]
pub enum EmulateCommand {
#[command(
long_about = "Apply one or more device or network emulation overrides. Multiple \
overrides can be combined in a single command (e.g., viewport + network + user \
agent). Overrides persist until 'emulate reset' is called or the browser is closed. \
Note: --geolocation and --no-geolocation are mutually exclusive, as are \
--user-agent and --no-user-agent.",
after_long_help = "\
EXAMPLES:
# Emulate a mobile device
agentchrome emulate set --viewport 375x667 --device-scale 2 --mobile
# Simulate slow network
agentchrome emulate set --network 3g
# Set geolocation (San Francisco)
agentchrome emulate set --geolocation 37.7749,-122.4194
# Force dark mode with custom user agent
agentchrome emulate set --color-scheme dark --user-agent \"CustomBot/1.0\"
# Throttle CPU (4x slowdown)
agentchrome emulate set --cpu 4"
)]
Set(EmulateSetArgs),
#[command(
long_about = "Clear all device and network emulation overrides, restoring the browser \
to its default settings. This removes viewport, user agent, geolocation, network \
throttling, CPU throttling, and color scheme overrides.",
after_long_help = "\
EXAMPLES:
# Reset all overrides
agentchrome emulate reset"
)]
Reset,
#[command(
long_about = "Display the current emulation state including viewport dimensions, \
user agent, device scale factor, network conditions, CPU throttling, and color \
scheme. Returns JSON with the active emulation configuration.",
after_long_help = "\
EXAMPLES:
# Check emulation status
agentchrome emulate status"
)]
Status,
}
#[allow(clippy::struct_excessive_bools)]
#[derive(Args)]
pub struct EmulateSetArgs {
#[arg(long, value_enum)]
pub network: Option<NetworkProfile>,
#[arg(long, value_parser = clap::value_parser!(u32).range(1..=20))]
pub cpu: Option<u32>,
#[arg(long, conflicts_with = "no_geolocation")]
pub geolocation: Option<String>,
#[arg(long, conflicts_with = "geolocation")]
pub no_geolocation: bool,
#[arg(long, conflicts_with = "no_user_agent")]
pub user_agent: Option<String>,
#[arg(long, conflicts_with = "user_agent")]
pub no_user_agent: bool,
#[arg(long, value_enum)]
pub color_scheme: Option<ColorScheme>,
#[arg(long)]
pub viewport: Option<String>,
#[arg(long)]
pub device_scale: Option<f64>,
#[arg(long)]
pub mobile: bool,
}
#[derive(Debug, Clone, Copy, ValueEnum)]
pub enum NetworkProfile {
Offline,
#[value(name = "slow-4g")]
Slow4g,
#[value(name = "4g")]
FourG,
#[value(name = "3g")]
ThreeG,
None,
}
#[derive(Debug, Clone, Copy, ValueEnum)]
pub enum ColorScheme {
Dark,
Light,
Auto,
}
#[derive(Args)]
pub struct ConfigArgs {
#[command(subcommand)]
pub command: ConfigCommand,
}
#[derive(Subcommand)]
pub enum ConfigCommand {
#[command(
long_about = "Display the fully resolved configuration by merging all sources in \
priority order: CLI flags > environment variables > config file > defaults. \
Returns JSON showing every setting and its effective value. Useful for debugging \
which settings are active.",
after_long_help = "\
EXAMPLES:
# Show resolved config
agentchrome config show
# Show config from a specific file
agentchrome --config ./my-config.toml config show"
)]
Show,
#[command(
long_about = "Create a new configuration file with all available settings documented \
as comments. By default, the file is created at the XDG config directory \
(~/.config/agentchrome/config.toml on Linux, ~/Library/Application Support/\
agentchrome/config.toml on macOS). Use --path to specify a custom location. \
Will not overwrite an existing file.",
after_long_help = "\
EXAMPLES:
# Create default config file
agentchrome config init
# Create at a custom path
agentchrome config init --path ./my-config.toml"
)]
Init(ConfigInitArgs),
#[command(
long_about = "Show the path of the active configuration file. Searches in priority \
order: --config flag, $AGENTCHROME_CONFIG env var, project-local \
(.agentchrome.toml), XDG config dir, home directory (~/.agentchrome.toml). \
Returns JSON with {\"path\": \"...\"} or {\"path\": null} if no config file is found.",
after_long_help = "\
EXAMPLES:
# Show active config path
agentchrome config path"
)]
Path,
}
#[derive(Args)]
pub struct ConfigInitArgs {
#[arg(long)]
pub path: Option<PathBuf>,
}
#[derive(Args)]
pub struct CompletionsArgs {
pub shell: Shell,
}
#[derive(Args)]
pub struct ManArgs {
pub command: Option<String>,
}
#[derive(Args)]
pub struct ExamplesArgs {
pub command: Option<String>,
pub name: Option<String>,
}
#[derive(Args)]
pub struct CapabilitiesArgs {
pub command: Option<String>,
#[arg(long)]
pub compact: bool,
}
#[cfg(test)]
mod tests {
use super::Cli;
use clap::Parser;
fn try_parse(cmd: &str) -> Result<Cli, clap::Error> {
let parts: Vec<&str> = std::iter::once("agentchrome")
.chain(cmd.split_whitespace())
.collect();
Cli::try_parse_from(parts)
}
#[test]
fn diagnose_no_args_is_error() {
assert!(
try_parse("diagnose").is_err(),
"Expected a clap error when neither URL nor --current is supplied"
);
}
#[test]
fn diagnose_url_and_current_is_error() {
assert!(
try_parse("diagnose https://example.com --current").is_err(),
"Expected a clap error when both URL and --current are supplied"
);
}
#[test]
fn diagnose_url_only_parses() {
let cli = try_parse("diagnose https://example.com").expect("URL-only form should parse");
let super::Command::Diagnose(args) = cli.command else {
panic!("Expected Diagnose command variant");
};
assert_eq!(args.url.as_deref(), Some("https://example.com"));
assert!(!args.current);
}
#[test]
fn diagnose_current_only_parses() {
let cli = try_parse("diagnose --current").expect("--current-only form should parse");
let super::Command::Diagnose(args) = cli.command else {
panic!("Expected Diagnose command variant");
};
assert!(args.current);
assert!(args.url.is_none());
}
#[test]
fn diagnose_current_before_url_is_error() {
assert!(
try_parse("diagnose --current https://example.com").is_err(),
"Expected a clap error when --current appears before the URL"
);
}
#[test]
fn examples_strategies_no_name_parses() {
let cli = try_parse("examples strategies").expect("examples strategies should parse");
let super::Command::Examples(args) = cli.command else {
panic!("Expected Examples command variant");
};
assert_eq!(args.command.as_deref(), Some("strategies"));
assert!(args.name.is_none());
}
#[test]
fn examples_strategies_with_name_parses() {
let cli = try_parse("examples strategies iframes")
.expect("examples strategies iframes should parse");
let super::Command::Examples(args) = cli.command else {
panic!("Expected Examples command variant");
};
assert_eq!(args.command.as_deref(), Some("strategies"));
assert_eq!(args.name.as_deref(), Some("iframes"));
}
#[test]
fn examples_navigate_still_parses() {
let cli = try_parse("examples navigate").expect("examples navigate should still parse");
let super::Command::Examples(args) = cli.command else {
panic!("Expected Examples command variant");
};
assert_eq!(args.command.as_deref(), Some("navigate"));
assert!(args.name.is_none());
}
#[test]
fn examples_subcommand_carries_clap_help_metadata() {
use clap::CommandFactory;
let cmd = Cli::command();
let examples_subcmd = cmd
.get_subcommands()
.find(|s| s.get_name() == "examples")
.expect("examples subcommand must exist");
let long_about = examples_subcmd
.get_long_about()
.expect("examples must have long_about");
let long_about_str = long_about.to_string();
assert!(
!long_about_str.is_empty(),
"examples long_about must not be empty"
);
assert!(
long_about_str.contains("strategies"),
"examples long_about must mention 'strategies'\ngot: {long_about_str}"
);
let after_long_help = examples_subcmd
.get_after_long_help()
.expect("examples must have after_long_help");
let after_str = after_long_help.to_string();
assert!(
after_str.contains("examples strategies"),
"examples after_long_help must contain 'examples strategies'\ngot: {after_str}"
);
assert!(
after_str.contains("--json"),
"examples after_long_help must contain at least one --json example\ngot: {after_str}"
);
}
#[test]
fn capabilities_with_positional_parses() {
let cli = try_parse("capabilities page").expect("capabilities page should parse");
let super::Command::Capabilities(args) = cli.command else {
panic!("Expected Capabilities command variant");
};
assert_eq!(args.command.as_deref(), Some("page"));
assert!(!args.compact);
}
#[test]
fn capabilities_no_args_parses() {
let cli = try_parse("capabilities").expect("capabilities should parse");
let super::Command::Capabilities(args) = cli.command else {
panic!("Expected Capabilities command variant");
};
assert!(args.command.is_none());
}
#[test]
fn capabilities_subcommand_carries_clap_help_metadata() {
use clap::CommandFactory;
let cmd = Cli::command();
let caps = cmd
.get_subcommands()
.find(|s| s.get_name() == "capabilities")
.expect("capabilities subcommand must exist");
let long_about = caps
.get_long_about()
.expect("capabilities must have long_about");
let long_about_str = long_about.to_string();
assert!(
long_about_str.contains("detail") || long_about_str.contains("listing"),
"capabilities long_about must mention 'detail' or 'listing'\ngot: {long_about_str}"
);
let after = caps
.get_after_long_help()
.expect("capabilities must have after_long_help");
let after_str = after.to_string();
assert!(
after_str.contains("capabilities"),
"capabilities after_long_help must contain 'capabilities'\ngot: {after_str}"
);
assert!(
after_str.contains("--json"),
"capabilities after_long_help must contain a --json example\ngot: {after_str}"
);
}
#[test]
fn cookie_set_accepts_url_alias() {
let cli = try_parse("cookie set n v --url https://example.com/path")
.expect("cookie set --url should parse");
let super::Command::Cookie(args) = cli.command else {
panic!("expected Cookie command");
};
let super::CookieCommand::Set(set) = args.command else {
panic!("expected CookieCommand::Set");
};
assert_eq!(set.url.as_deref(), Some("https://example.com/path"));
assert!(set.domain.is_none());
}
#[test]
fn cookie_set_rejects_url_and_domain_together() {
assert!(
Cli::try_parse_from([
"agentchrome",
"cookie",
"set",
"n",
"v",
"--url",
"https://example.com/",
"--domain",
"other.com",
])
.is_err(),
"clap conflicts_with should reject --url + --domain"
);
}
#[test]
fn tabs_close_accepts_repeated_tab_flags() {
let cli = try_parse("tabs close --tab A --tab B").expect("--tab repeats should parse");
let super::Command::Tabs(args) = cli.command else {
panic!("expected Tabs command");
};
let super::TabsCommand::Close(close) = args.command else {
panic!("expected TabsCommand::Close");
};
assert!(close.targets.is_empty());
assert_eq!(close.tab, vec!["A".to_string(), "B".to_string()]);
}
#[test]
fn tabs_close_no_args_still_parses() {
let cli = try_parse("tabs close").expect("tabs close with no args should parse");
let super::Command::Tabs(args) = cli.command else {
panic!("expected Tabs command");
};
let super::TabsCommand::Close(close) = args.command else {
panic!("expected TabsCommand::Close");
};
assert!(close.targets.is_empty());
assert!(close.tab.is_empty());
}
#[test]
fn dom_query_is_alias_for_select() {
let cli = try_parse("dom query h1").expect("dom query should parse as dom select");
let super::Command::Dom(args) = cli.command else {
panic!("expected Dom command");
};
let super::DomCommand::Select(sel) = args.command else {
panic!("expected DomCommand::Select via alias");
};
assert_eq!(sel.selector, "h1");
assert!(!sel.xpath);
}
#[test]
fn dom_query_supports_xpath_flag() {
let cli =
try_parse("dom query //a[@href] --xpath").expect("dom query should support --xpath");
let super::Command::Dom(args) = cli.command else {
panic!("expected Dom command");
};
let super::DomCommand::Select(sel) = args.command else {
panic!("expected DomCommand::Select via alias");
};
assert_eq!(sel.selector, "//a[@href]");
assert!(sel.xpath);
}
#[test]
fn cookie_set_help_does_not_mention_url_alias() {
use clap::CommandFactory;
let cmd = Cli::command();
let cookie = cmd
.get_subcommands()
.find(|s| s.get_name() == "cookie")
.expect("cookie subcommand exists");
let set = cookie
.get_subcommands()
.find(|s| s.get_name() == "set")
.expect("cookie set subcommand exists");
let url_arg = set
.get_arguments()
.find(|a| a.get_id() == "url")
.expect("url arg exists on cookie set");
assert!(url_arg.is_hide_set(), "--url must be hidden from help");
}
#[test]
fn dom_query_alias_is_hidden() {
use clap::CommandFactory;
let cmd = Cli::command();
let dom = cmd
.get_subcommands()
.find(|s| s.get_name() == "dom")
.expect("dom subcommand exists");
let select = dom
.get_subcommands()
.find(|s| s.get_name() == "select")
.expect("dom select subcommand exists");
let aliases: Vec<&str> = select.get_all_aliases().collect();
assert!(
aliases.contains(&"query"),
"dom select must alias 'query', got {aliases:?}"
);
let visible: Vec<&str> = select.get_visible_aliases().collect();
assert!(
!visible.contains(&"query"),
"dom select 'query' alias must be hidden, not visible"
);
}
}