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