1use 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#[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
26pub 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#[derive(Debug)]
57pub enum BrowsersError {
58 Usage {
60 kind: UsageErrorKind,
62 message: String,
64 },
65 Stage {
67 stderr: String,
69 exit_code: i32,
71 },
72 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}