Skip to main content

agent_first_http/cli/cmd/
ui.rs

1//! `afhttp ui` subcommand. Prints the ops panel URL for an endpoint.
2
3use clap::Args as ClapArgs;
4use serde::Serialize;
5
6use crate::cli::output;
7use crate::sdk::endpoint::Endpoint;
8use crate::shared::error::Error;
9
10#[derive(ClapArgs, Debug)]
11pub struct Args {
12    /// CDP endpoint of the running host.
13    #[arg(long = "endpoint-url")]
14    pub endpoint: String,
15    /// Bearer token, if the host requires one (appended to the panel URLs).
16    #[arg(long = "token-secret")]
17    pub token: Option<String>,
18}
19
20#[derive(Serialize)]
21struct UiResult {
22    panel_url: String,
23    display_url: String,
24}
25
26pub async fn run(args: Args) -> Result<(), Error> {
27    let result = build_result(&args.endpoint, args.token.as_deref())?;
28    output::emit("ui", &result)
29}
30
31fn build_result(endpoint: &str, token: Option<&str>) -> Result<UiResult, Error> {
32    let endpoint = Endpoint::parse(endpoint)?;
33    let base = endpoint.http_base();
34    let mut panel_url = url::Url::parse(&format!("{base}/ops")).map_err(|e| {
35        crate::shared::error::Error::new(
36            crate::shared::error::ErrorCode::InvalidEndpoint,
37            format!("ui panel URL from endpoint {base:?}: {e}"),
38        )
39    })?;
40    let mut display_url = url::Url::parse(&format!("{base}/ops/display")).map_err(|e| {
41        crate::shared::error::Error::new(
42            crate::shared::error::ErrorCode::InvalidEndpoint,
43            format!("ui display URL from endpoint {base:?}: {e}"),
44        )
45    })?;
46    if let Some(token) = token {
47        panel_url
48            .query_pairs_mut()
49            .append_pair("token_secret", token);
50        display_url
51            .query_pairs_mut()
52            .append_pair("token_secret", token);
53    }
54    Ok(UiResult {
55        panel_url: panel_url.to_string(),
56        display_url: display_url.to_string(),
57    })
58}
59
60#[cfg(test)]
61mod tests {
62    use super::*;
63
64    #[test]
65    fn ui_result_has_no_url_alias() {
66        let value = serde_json::to_value(UiResult {
67            panel_url: "http://localhost:9222/ops".into(),
68            display_url: "http://localhost:9222/ops/display".into(),
69        })
70        .unwrap();
71        assert!(value.get("url").is_none());
72        assert!(value.get("panel_url").is_some());
73        assert!(value.get("display_url").is_some());
74    }
75
76    #[test]
77    fn ui_token_query_is_percent_encoded() {
78        let result = build_result("http://localhost:9222", Some("a+b&c%20")).unwrap();
79        assert_eq!(
80            result.panel_url,
81            "http://localhost:9222/ops?token_secret=a%2Bb%26c%2520"
82        );
83        assert_eq!(
84            result.display_url,
85            "http://localhost:9222/ops/display?token_secret=a%2Bb%26c%2520"
86        );
87    }
88}