1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
//! The subcommand enums: what each verb of the CLI can be asked to do.
//!
//! Split out of `cli.rs` for the repo's 1000-line file cap and re-exported from it, so every
//! call site stays `crate::cli::EmulateAction`. The seam is the natural one: `cli.rs` declares
//! the flags and the verbs, and these declare what the verbs that have modes accept.
use clap::Subcommand;
#[derive(Subcommand)]
pub enum MacroAction {
/// List the macros this machine knows
List,
/// Print one macro, with its steps and their guards
Show {
/// Macro name
name: String,
},
/// Distil a recorded session into a macro
Record {
/// Name to save it under
name: String,
/// The recording to distil (a `_record` file written by a pipe session)
#[arg(long)]
from_recording: String,
/// Index of the first entry of the task (default: the last successful navigation)
#[arg(long)]
from: Option<usize>,
},
/// Run a macro, guarding every step
Run {
/// Macro name
name: String,
/// Parameter values (repeatable, or comma-separated): --var email=a@b.c
#[arg(long, value_delimiter = ',')]
var: Vec<String>,
},
}
#[derive(Subcommand)]
pub enum EmulateAction {
/// Apply and persist explicit metrics for this named page
Device {
/// Optional display label reported by `emulate status`
#[arg(long)]
label: Option<String>,
/// Emulated device width in CSS pixels
#[arg(long)]
width: u32,
/// Emulated device height in CSS pixels
#[arg(long)]
height: u32,
/// Device pixel ratio; must be finite and greater than zero
#[arg(long, default_value = "1")]
dpr: f64,
/// Enable Chromium's mobile viewport mode
#[arg(long)]
mobile: bool,
/// Advertise touch support and dispatch clicks as touch taps
#[arg(long)]
touch: bool,
/// Screen orientation; inferred from width and height when omitted
#[arg(long)]
orientation: Option<crate::emulation::DeviceOrientation>,
},
/// Show requested metrics and values currently observed by the page
Status,
/// Clear target overrides and the persisted metrics for this named page
Reset,
}
#[derive(Subcommand)]
pub enum WebmcpAction {
/// List tools this page has registered — name, description, inputSchema. No outputSchema:
/// the protocol defines none.
List,
/// Call a tool by name (resolved to the `RegisteredTool` the page's own `getTools()` reported —
/// executeTool refuses a bare name). Reports what the tool declared AND what the page's
/// accessibility tree measurably did, side by side.
Call {
/// Tool name, exactly as `webmcp list` reported it
name: String,
/// JSON object of arguments (default: "{}")
#[arg(long, default_value = "{}")]
args: String,
/// Inspect page after calling
#[arg(long)]
inspect: bool,
/// Max depth for inspect output (also accepted as global flag)
#[arg(long)]
max_depth: Option<usize>,
},
}
#[derive(Subcommand)]
pub enum DaemonAction {
/// Start the daemon (foreground, used internally)
Start,
}
/// What `assert` can be asked about the page.
///
/// One subcommand per kind of claim rather than a pile of flags on `assert`: the comparators
/// that make sense differ per kind (a URL is equal or matches a pattern; a page's text is
/// contained or matches), and clap's arg groups can then enforce "exactly one comparator"
/// and "exactly one state" instead of the dispatcher discovering it at run time.
#[derive(Subcommand)]
pub enum AssertWhat {
/// A form control's value (input, textarea, select)
#[command(group = clap::ArgGroup::new("value_cmp").required(true).args(["equals", "contains", "matches"]))]
#[command(group = clap::ArgGroup::new("value_target").required(true).args(["selector", "uid"]))]
Value {
/// CSS selector of the control
#[arg(long)]
selector: Option<String>,
/// Element uid of the control (e.g. "n47")
#[arg(long)]
uid: Option<String>,
/// The value the control must hold, exactly
#[arg(long)]
equals: Option<String>,
/// A substring the value must contain
#[arg(long)]
contains: Option<String>,
/// A regular expression the value must match (Rust regex; \d \w \s are ASCII-only)
#[arg(long)]
matches: Option<String>,
},
/// Visible text — of the whole page, or of one element with --selector/--uid
#[command(group = clap::ArgGroup::new("text_cmp").required(true).args(["contains", "matches"]))]
#[command(group = clap::ArgGroup::new("text_target").args(["selector", "uid"]))]
Text {
/// CSS selector to scope the text to (default: the whole page)
#[arg(long)]
selector: Option<String>,
/// Element uid to scope the text to
#[arg(long)]
uid: Option<String>,
/// A substring the text must contain
#[arg(long)]
contains: Option<String>,
/// A regular expression the text must match
#[arg(long)]
matches: Option<String>,
},
/// The current URL
#[command(group = clap::ArgGroup::new("url_cmp").required(true).args(["equals", "matches"]))]
Url {
/// The URL the page must be on, exactly
#[arg(long)]
equals: Option<String>,
/// A regular expression the URL must match
#[arg(long)]
matches: Option<String>,
},
/// An element's state: checked, selected, enabled or rendered
#[command(group = clap::ArgGroup::new("state_want").required(true).args(["checked", "unchecked", "selected", "enabled", "disabled", "visible"]))]
#[command(group = clap::ArgGroup::new("state_target").required(true).args(["selector", "uid"]))]
State {
/// CSS selector of the element
#[arg(long)]
selector: Option<String>,
/// Element uid of the element
#[arg(long)]
uid: Option<String>,
/// The checkbox/radio (native or ARIA) must be checked
#[arg(long)]
checked: bool,
/// The checkbox/radio must be unchecked (indeterminate satisfies neither)
#[arg(long)]
unchecked: bool,
/// The <select> must hold this option (by value or visible text)
#[arg(long)]
selected: Option<String>,
/// The control must not be disabled (neither :disabled nor aria-disabled)
#[arg(long)]
enabled: bool,
/// The control must be disabled (:disabled or aria-disabled)
#[arg(long)]
disabled: bool,
/// The element must be rendered, opaque and not visibility:hidden
#[arg(long)]
visible: bool,
},
/// How many elements a CSS selector matches
#[command(group = clap::ArgGroup::new("exists_count").args(["count", "min"]))]
Exists {
/// CSS selector to count
#[arg(long)]
selector: String,
/// Exactly this many matches (0 asserts absence)
#[arg(long)]
count: Option<usize>,
/// At least this many matches
#[arg(long)]
min: Option<usize>,
},
}