Files
BrowserSkill/crates/bsk-cli/src/main.rs
T
BB-fat 0f264c5df1 feat(emulate): add mobile device emulation via CDP Emulation domain (#60)
* feat(emulate): add mobile device emulation via CDP Emulation domain

Add `bsk emulate` so agents can debug mobile page behaviour by
emulating a device environment on a session tab:

- Viewport metrics (Emulation.setDeviceMetricsOverride), User-Agent
  (Emulation.setUserAgentOverride, optional accept-language and UA
  client-hints metadata), and touch (Emulation.setTouchEmulationEnabled
  with maxTouchPoints).
- Seven CLI-side device presets (iphone-14, iphone-14-pro-max,
  iphone-se, pixel-7, galaxy-s23, ipad-mini, galaxy-tab-s8) via
  --device; manual flags (--width/--height/--dpr/--mobile/--ua/
  --accept-language/--touch/--max-touch-points) work standalone or
  override individual preset fields.
- `bsk emulate --off` clears every override (clearDeviceMetricsOverride
  + touch disabled + empty UA override) and restores the real
  environment.
- Overrides are per-tab (CDP per-target) and not inherited by new tabs;
  the result JSON carries a note reminding agents to re-apply.

Wire: new tool.emulate method end-to-end (bsk-protocol types + JSON
schemas + Method::ToolEmulate classified BrowserMutation, daemon
forwarding, CLI subcommand, extension tool handler + ChromiumCdp
Emulation wrappers, dispatcher registration), docs in both SKILL.md
copies, and unit tests on every layer.

* refine(emulate): merge per-tab state and align preset override semantics

- extension: remember the applied emulation state per tab and merge each new request onto it field by field before applying, so a later --width/--height no longer resets an earlier preset's dpr/mobile; --off clears the stored state along with the CDP overrides
- extension: cdp_failed errors now note that already-applied overrides were not rolled back and can be reset with --off
- cli: validate width/height pairing and metric requirements on the merged preset+flags result, so --device iphone-14 --width 500 keeps the preset height
- cli: add --no-mobile/--no-touch (conflicting with --mobile/--touch) to turn preset mobile/touch fields back off
- docs: describe the merge behaviour, --no-mobile/--no-touch and --off state clearing in both SKILL.md copies
2026-08-05 18:10:39 +08:00

125 lines
4.7 KiB
Rust

use std::process::ExitCode;
use clap::{Parser, error::ErrorKind};
use bsk::cli::error::{CliError, Format, render};
use bsk::cli::status::Output;
use bsk::{Cli, Command, cli};
fn main() -> ExitCode {
let cli = match Cli::try_parse() {
Ok(cli) => cli,
Err(err) => {
let exit = match err.kind() {
ErrorKind::DisplayHelp | ErrorKind::DisplayVersion => 0,
_ => 1,
};
let _ = err.print();
return ExitCode::from(exit);
}
};
// The daemon installs its own subscriber (with the rolling file
// appender); CLI tracing would race with it and silently win, so
// only init the CLI-side stderr subscriber for client commands.
if !matches!(cli.command, Command::Daemon(_)) {
init_cli_tracing(&cli.flags);
}
cli::update::maybe_spawn_background_check(&cli.flags, &cli.command);
let format = if cli.flags.json {
Format::Json
} else {
Format::Human
};
match dispatch(cli, format) {
Ok(()) => ExitCode::SUCCESS,
Err(err) => render(&err, format),
}
}
fn dispatch(cli: Cli, format: Format) -> Result<(), CliError> {
match cli.command {
Command::Daemon(cmd) => cli::daemon::dispatch(cmd).map_err(CliError::Local),
Command::Status => {
let output = if cli.flags.json {
Output::Json
} else {
Output::Human
};
cli::status::run(output).map(|_| ())
}
Command::Doctor => {
let output = if cli.flags.json {
Output::Json
} else {
Output::Human
};
let checks = cli::doctor::run(output).map_err(CliError::Local)?;
if cli::doctor::has_failures(&checks) {
Err(CliError::RenderedExit { exit_code: 1 })
} else {
Ok(())
}
}
Command::InstallSkill(args) => {
let output = if cli.flags.json {
Output::Json
} else {
Output::Human
};
cli::install_skill::dispatch(args, output)
}
Command::Update(args) => cli::update::dispatch(args, format),
Command::Logs(cmd) => cli::logs::run(cli::logs::LogsArgs {
follow: cmd.follow,
lines: cmd.lines,
})
.map_err(CliError::Local),
Command::Session(cmd) => cli::session::dispatch(cmd, format),
Command::Browsers => cli::browsers::dispatch(format),
Command::Tab(cmd) => cli::tab::dispatch(cmd, format),
Command::Window(cmd) => cli::window::dispatch(cmd, format),
Command::Emulate(args) => cli::emulate::dispatch(args, format),
Command::Screenshot(args) => cli::screenshot::dispatch(args, format),
Command::Snapshot(args) => cli::snapshot::dispatch(args, format),
Command::Observe(args) => cli::observe::dispatch(args, format),
Command::Console(args) => cli::console::dispatch(args, format),
Command::Network(args) => cli::network::dispatch(args, format),
Command::GetHtml(args) => cli::get_html::dispatch(args, format),
Command::Navigate(args) => cli::navigate::dispatch_navigate_command(args, format),
Command::NavigateBack(args) => cli::navigate::dispatch_navigate_back(args, format),
Command::NavigateForward(args) => cli::navigate::dispatch_navigate_forward(args, format),
Command::Reload(args) => cli::navigate::dispatch_reload(args, format),
Command::Click(args) => cli::interaction::dispatch_click(args, format),
Command::Fill(args) => cli::interaction::dispatch_fill(args, format),
Command::Press(args) => cli::interaction::dispatch_press(args, format),
Command::Select(args) => cli::interaction::dispatch_select(args, format),
Command::Evaluate(args) => cli::evaluate::dispatch(args, format),
Command::WaitForNavigation(args) => cli::waits::dispatch_wait_for_navigation(args, format),
Command::WaitMs(args) => cli::waits::dispatch_wait_ms(args, format),
Command::RequestHelp(args) => cli::human_loop::dispatch(args, format),
Command::Record(cmd) => cli::record::dispatch(cmd, format),
}
}
fn init_cli_tracing(flags: &bsk::cli::GlobalFlags) {
use tracing_subscriber::EnvFilter;
let level = if flags.quiet {
"warn"
} else {
match flags.verbose {
0 => "info",
1 => "debug",
_ => "trace",
}
};
let filter = EnvFilter::try_from_default_env()
.unwrap_or_else(|_| EnvFilter::new(format!("bsk={level}")));
let _ = tracing_subscriber::fmt()
.with_env_filter(filter)
.with_writer(std::io::stderr)
.try_init();
}