Skip to main content

wyvern/
browsers_cmd.rs

1//! `wyvern browsers list|refresh` — inspect / rebuild the local browser registry.
2
3use wyvern_host::{
4    browser_registry_path, list_browser_entries, refresh_browser_registry, BrowserRegistryEntry,
5    HostError,
6};
7
8use crate::error::{emit_host_error, BuiltinDomain, EmitError, UsageErrorKind};
9
10/// Usage text for `wyvern browsers --help` / `-h`.
11#[must_use]
12pub fn browsers_usage_message() -> String {
13    concat!(
14        "Usage: wyvern browsers list|refresh\n",
15        "       wyvern browsers --help\n",
16        "\n",
17        "Commands:\n",
18        "  list      List browsers in the local registry\n",
19        "  refresh   Rebuild the local browser registry\n",
20        "\n",
21        "See also: wyvern --help\n",
22    )
23    .to_string()
24}
25
26/// Run a `browsers` subcommand; returns stdout text on success.
27///
28/// # Errors
29///
30/// Returns structured stderr + exit code on registry failure.
31pub fn run_browsers_command(args: &[String]) -> Result<String, BrowsersError> {
32    if args
33        .first()
34        .is_some_and(|token| token == "--help" || token == "-h")
35    {
36        return Ok(browsers_usage_message());
37    }
38    let sub = args.first().map(String::as_str).unwrap_or("list");
39    match sub {
40        "list" => list(),
41        "refresh" => refresh(),
42        other => Err(BrowsersError::Usage {
43            kind: UsageErrorKind::UnknownSubcommand {
44                domain: BuiltinDomain::Browsers,
45                token: other.to_string(),
46            },
47            message: format!(
48                "unknown browsers subcommand '{other}'\n{}",
49                browsers_usage_message()
50            ),
51        }),
52    }
53}
54
55/// CLI browsers subcommand failure.
56#[derive(Debug)]
57pub enum BrowsersError {
58    /// Bad argv.
59    Usage {
60        /// Discriminated usage class for structured stderr recovery.
61        kind: UsageErrorKind,
62        /// Plain-text usage.
63        message: String,
64    },
65    /// Registry / host failure with stderr JSON.
66    Stage {
67        /// Stderr JSON.
68        stderr: String,
69        /// Process exit code.
70        exit_code: i32,
71    },
72    /// Emit-boundary serialize failure.
73    Emit(EmitError),
74}
75
76fn list() -> Result<String, BrowsersError> {
77    let path = browser_registry_path();
78    let entries = list_browser_entries(&path).map_err(map_host)?;
79    Ok(format_entries(&entries))
80}
81
82fn refresh() -> Result<String, BrowsersError> {
83    let path = browser_registry_path();
84    let file = refresh_browser_registry(&path).map_err(map_host)?;
85    Ok(format!(
86        "Refreshed {} ({} entries)\n{}",
87        path.display(),
88        file.entries.len(),
89        format_entries(&file.entries)
90    ))
91}
92
93fn format_entries(entries: &[BrowserRegistryEntry]) -> String {
94    if entries.is_empty() {
95        return "No browsers found in registry.\nRun: wyvern browsers refresh".into();
96    }
97    let mut out = String::new();
98    for e in entries {
99        out.push_str(&format!(
100            "{:<10}  {:<20}  {}\n",
101            e.id,
102            e.name,
103            e.executable.display()
104        ));
105    }
106    out
107}
108
109fn map_host(err: HostError) -> BrowsersError {
110    match emit_host_error(&err) {
111        Ok(stderr) => {
112            let exit_code = match &err {
113                HostError::Bind { .. } => wyvern_schema::ErrorCode::HostBindError.exit_code(),
114                HostError::ViewerNotFound { .. } | HostError::ViewerUnsupported { .. } => {
115                    wyvern_schema::ErrorCode::HostViewerError.exit_code()
116                }
117                _ => wyvern_schema::ErrorCode::HostError.exit_code(),
118            };
119            BrowsersError::Stage { stderr, exit_code }
120        }
121        Err(e) => BrowsersError::Emit(e),
122    }
123}
124
125#[cfg(test)]
126mod tests {
127    use super::*;
128
129    #[test]
130    fn browsers_help_mentions_list_and_refresh() {
131        let text = browsers_usage_message();
132        assert!(text.contains("list"), "{text}");
133        assert!(text.contains("refresh"), "{text}");
134    }
135
136    #[test]
137    fn unknown_browsers_subcommand_is_discriminated() {
138        let err = run_browsers_command(&["nope".into()]).expect_err("usage");
139        match err {
140            BrowsersError::Usage { kind, message } => {
141                assert!(matches!(
142                    kind,
143                    UsageErrorKind::UnknownSubcommand { domain, ref token }
144                        if domain == BuiltinDomain::Browsers && token == "nope"
145                ));
146                assert!(message.contains("unknown browsers subcommand"), "{message}");
147            }
148            other => panic!("expected Usage, got {other:?}"),
149        }
150    }
151}