Skip to main content

agent_first_http/cli/cmd/
tabs.rs

1//! `afhttp tabs` — list and close CDP targets.
2
3use clap::{Args as ClapArgs, Subcommand};
4use serde_json::Value;
5
6use crate::cli::output;
7use crate::sdk::Client;
8use crate::shared::error::{Error, ErrorCode};
9
10#[derive(ClapArgs, Debug)]
11pub struct Args {
12    #[command(subcommand)]
13    pub sub: TabsSub,
14}
15
16#[derive(Subcommand, Debug)]
17#[command(disable_help_subcommand = true)]
18pub enum TabsSub {
19    /// List currently-attached CDP targets.
20    List(EndpointArgs),
21    /// Close a target by its CDP target id.
22    Close(CloseArgs),
23}
24
25#[derive(ClapArgs, Debug)]
26pub struct EndpointArgs {
27    /// CDP endpoint URL (e.g. `ws://127.0.0.1:9222`). Falls back to
28    /// `AFHTTP_ENDPOINT_URL`.
29    #[arg(long = "endpoint-url", env = "AFHTTP_ENDPOINT_URL")]
30    pub endpoint: String,
31    /// Bearer token, if the host was started with `--token-secret`.
32    /// Falls back to `AFHTTP_TOKEN_SECRET`.
33    #[arg(long = "token-secret", env = "AFHTTP_TOKEN_SECRET")]
34    pub token: Option<String>,
35}
36
37#[derive(ClapArgs, Debug)]
38pub struct CloseArgs {
39    /// CDP target id (tab) to close (e.g. `41A0F1E0FD…`).
40    #[arg(long)]
41    pub tab: String,
42    /// CDP endpoint URL (e.g. `ws://127.0.0.1:9222`). Falls back to
43    /// `AFHTTP_ENDPOINT_URL`.
44    #[arg(long = "endpoint-url", env = "AFHTTP_ENDPOINT_URL")]
45    pub endpoint: String,
46    /// Bearer token, if the host was started with `--token-secret`.
47    /// Falls back to `AFHTTP_TOKEN_SECRET`.
48    #[arg(long = "token-secret", env = "AFHTTP_TOKEN_SECRET")]
49    pub token: Option<String>,
50}
51
52pub async fn run(args: Args) -> Result<(), Error> {
53    match args.sub {
54        TabsSub::List(a) => list(a).await,
55        TabsSub::Close(a) => close(a).await,
56    }
57}
58
59fn build_client(endpoint: &str, token: Option<String>) -> Result<Client, Error> {
60    let mut client = Client::connect(endpoint)?;
61    if let Some(t) = token {
62        client = client.with_token(t);
63    }
64    Ok(client)
65}
66
67async fn list(args: EndpointArgs) -> Result<(), Error> {
68    let client = build_client(&args.endpoint, args.token)?;
69    let response = client.cdp("Target.getTargets").send().await?;
70    // `Client.cdp(...).send()` unwraps the JSON-RPC `result` envelope, so
71    // `response` is the inner method result and we read `targetInfos`
72    // off it directly.
73    let targets = response
74        .get("targetInfos")
75        .cloned()
76        .unwrap_or(Value::Array(Vec::new()));
77    let payload = serde_json::json!({
78        "code": "tabs",
79        "targets": targets,
80    });
81    output::emit("tabs", &payload)
82}
83
84async fn close(args: CloseArgs) -> Result<(), Error> {
85    if args.tab.trim().is_empty() {
86        return Err(Error::new(
87            ErrorCode::InvalidArgument,
88            "tabs close: target id must not be empty",
89        ));
90    }
91    let client = build_client(&args.endpoint, args.token)?;
92    let response = client
93        .cdp("Target.closeTarget")
94        .params(serde_json::json!({ "targetId": args.tab }))
95        .send()
96        .await?;
97    let success = response
98        .get("success")
99        .and_then(Value::as_bool)
100        .unwrap_or(false);
101    let payload = serde_json::json!({
102        "code": "tab_closed",
103        "target_id": args.tab,
104        "success": success,
105    });
106    output::emit("tab_closed", &payload)
107}