Skip to main content

ubt_cli/commands/
tool.rs

1use std::path::Path;
2use std::process;
3
4use crate::cli::{Cli, ToolCommand};
5use crate::config::{UbtConfig, load_config};
6use crate::detect::detect_tool;
7use crate::error::UbtError;
8use crate::plugin::PluginRegistry;
9
10use super::info::cmd_info;
11
12fn cmd_doctor(
13    cli: &Cli,
14    config: Option<&UbtConfig>,
15    project_root: &Path,
16    registry: &PluginRegistry,
17) -> Result<(), UbtError> {
18    let quiet = cli.quiet;
19    let mut any_fail = false;
20
21    macro_rules! check_ok {
22        ($msg:expr) => {
23            if !quiet {
24                println!("[ok]   {}", $msg);
25            }
26        };
27    }
28    macro_rules! check_warn {
29        ($msg:expr) => {
30            if !quiet {
31                println!("[warn] {}", $msg);
32            }
33        };
34    }
35    macro_rules! check_fail {
36        ($msg:expr) => {{
37            any_fail = true;
38            eprintln!("[fail] {}", $msg);
39        }};
40    }
41
42    // ── 1. Check detected tool binary ────────────────────────────────────
43    let config_tool = config.and_then(|c| c.project.as_ref()?.tool.as_deref());
44    match detect_tool(cli.tool.as_deref(), config_tool, project_root, registry) {
45        Err(e) => {
46            check_fail!(format!("Tool detection failed: {e}"));
47        }
48        Ok(detection) => {
49            if let Some((plugin, _)) = registry.get(&detection.plugin_name)
50                && let Some(variant) = plugin.variants.get(&detection.variant_name)
51            {
52                match which::which(&variant.binary) {
53                    Ok(path) => {
54                        // Try to get version
55                        let version = std::process::Command::new(&variant.binary)
56                            .arg("--version")
57                            .output()
58                            .ok()
59                            .and_then(|o| {
60                                let out = if o.stdout.is_empty() {
61                                    o.stderr
62                                } else {
63                                    o.stdout
64                                };
65                                String::from_utf8(out).ok()
66                            })
67                            .map(|s| s.lines().next().unwrap_or("").trim().to_string())
68                            .filter(|s| !s.is_empty())
69                            .unwrap_or_else(|| "version unknown".to_string());
70                        check_ok!(format!(
71                            "{} binary: {} ({})",
72                            variant.binary,
73                            path.display(),
74                            version
75                        ));
76                    }
77                    Err(_) => {
78                        let hint = plugin
79                            .install_help
80                            .as_deref()
81                            .map(|h| format!(" — install: {h}"))
82                            .unwrap_or_default();
83                        check_fail!(format!("{} is not installed{}", variant.binary, hint));
84                    }
85                }
86            }
87        }
88    }
89
90    // ── 2. Validate ubt.toml ────────────────────────────────────────────
91    let cwd = std::env::current_dir()?;
92    match load_config(&cwd) {
93        Err(e) => check_fail!(format!("ubt.toml parse error: {e}")),
94        Ok(None) => check_warn!("No ubt.toml found in this directory tree"),
95        Ok(Some((cfg, config_root))) => {
96            check_ok!(format!(
97                "ubt.toml valid at {}",
98                config_root.join("ubt.toml").display()
99            ));
100
101            // ── 3. Verify alias targets exist ──────────────────────────
102            for (alias, target) in &cfg.aliases {
103                let first_word = target.split_whitespace().next().unwrap_or(target.as_str());
104                let known_commands = [
105                    "dep.install",
106                    "dep.install_pkg",
107                    "dep.remove",
108                    "dep.update",
109                    "dep.outdated",
110                    "dep.list",
111                    "dep.audit",
112                    "dep.lock",
113                    "dep.why",
114                    "build",
115                    "build.dev",
116                    "start",
117                    "run",
118                    "run-file",
119                    "exec",
120                    "test",
121                    "lint",
122                    "fmt",
123                    "check",
124                    "clean",
125                    "release",
126                    "publish",
127                    "db.migrate",
128                    "db.rollback",
129                    "db.seed",
130                    "db.create",
131                    "db.drop",
132                    "db.reset",
133                    "db.status",
134                ];
135                if known_commands.contains(&first_word)
136                    || cfg.aliases.contains_key(first_word)
137                    || cfg.commands.contains_key(first_word)
138                {
139                    check_ok!(format!("alias '{alias}' → valid target '{first_word}'"));
140                } else {
141                    check_warn!(format!(
142                        "alias '{alias}' target '{first_word}' is not a known ubt command"
143                    ));
144                }
145            }
146        }
147    }
148
149    if any_fail {
150        process::exit(1);
151    }
152
153    Ok(())
154}
155
156pub fn cmd_tool(
157    sub: &ToolCommand,
158    cli: &Cli,
159    config: Option<&UbtConfig>,
160    project_root: &std::path::Path,
161    registry: &PluginRegistry,
162) -> Result<(), UbtError> {
163    match sub {
164        ToolCommand::Info => cmd_info(cli, config, project_root, registry),
165        ToolCommand::Doctor => cmd_doctor(cli, config, project_root, registry),
166        ToolCommand::List => {
167            if !cli.quiet {
168                println!("{:<12} {:<30} Variants", "Plugin", "Description");
169                println!("{}", "-".repeat(70));
170                let mut names: Vec<_> = registry.names();
171                names.sort();
172                for name in names {
173                    if let Some((plugin, _)) = registry.get(name) {
174                        let variants: Vec<_> = plugin.variants.keys().cloned().collect();
175                        println!(
176                            "{:<12} {:<30} {}",
177                            plugin.name,
178                            plugin.description,
179                            variants.join(", ")
180                        );
181                    }
182                }
183            }
184            Ok(())
185        }
186        ToolCommand::Docs(args) => {
187            let config_tool = config.and_then(|c| c.project.as_ref()?.tool.as_deref());
188            let detection = detect_tool(cli.tool.as_deref(), config_tool, project_root, registry)?;
189            if let Some((plugin, _)) = registry.get(&detection.plugin_name) {
190                if let Some(hp) = &plugin.homepage {
191                    if args.open {
192                        if let Err(e) = open::that(hp) {
193                            eprintln!("ubt: could not open browser: {e}");
194                            if !cli.quiet {
195                                println!("{hp}");
196                            }
197                        }
198                    } else if !cli.quiet {
199                        println!("{hp}");
200                    }
201                } else if !cli.quiet {
202                    println!("No documentation URL configured for {}", plugin.name);
203                }
204            }
205            Ok(())
206        }
207    }
208}
209
210#[cfg(test)]
211mod tests {
212    use super::*;
213    use crate::cli::{Command, DocsArgs};
214    use crate::plugin::{PluginRegistry, PluginSource};
215    use tempfile::TempDir;
216
217    fn make_cli(quiet: bool, tool: Option<&str>) -> Cli {
218        Cli {
219            verbose: false,
220            quiet,
221            tool: tool.map(|s| s.to_string()),
222            command: Command::Info,
223        }
224    }
225
226    #[test]
227    fn cmd_tool_list_non_quiet_returns_ok() {
228        let registry = PluginRegistry::new().unwrap();
229        let dir = TempDir::new().unwrap();
230        let cli = make_cli(false, None);
231        let result = cmd_tool(&ToolCommand::List, &cli, None, dir.path(), &registry);
232        assert!(result.is_ok());
233    }
234
235    #[test]
236    fn cmd_tool_list_quiet_returns_ok() {
237        let registry = PluginRegistry::new().unwrap();
238        let dir = TempDir::new().unwrap();
239        let cli = make_cli(true, None);
240        let result = cmd_tool(&ToolCommand::List, &cli, None, dir.path(), &registry);
241        assert!(result.is_ok());
242    }
243
244    #[test]
245    fn cmd_tool_docs_with_homepage_returns_ok() {
246        let registry = PluginRegistry::new().unwrap();
247        let dir = TempDir::new().unwrap();
248        // go plugin has a homepage; cli.tool forces selection without needing go.mod
249        let cli = make_cli(false, Some("go"));
250        let docs_args = DocsArgs { open: false };
251        let result = cmd_tool(
252            &ToolCommand::Docs(docs_args),
253            &cli,
254            None,
255            dir.path(),
256            &registry,
257        );
258        assert!(result.is_ok());
259    }
260
261    #[test]
262    fn cmd_tool_docs_quiet_with_homepage_returns_ok() {
263        let registry = PluginRegistry::new().unwrap();
264        let dir = TempDir::new().unwrap();
265        let cli = make_cli(true, Some("go"));
266        let docs_args = DocsArgs { open: false };
267        let result = cmd_tool(
268            &ToolCommand::Docs(docs_args),
269            &cli,
270            None,
271            dir.path(),
272            &registry,
273        );
274        assert!(result.is_ok());
275    }
276
277    #[test]
278    fn cmd_tool_docs_no_homepage_returns_ok() {
279        let dir = TempDir::new().unwrap();
280        // Create a plugin with no homepage
281        let toml = r#"
282[plugin]
283name = "nohp"
284
285[detect]
286files = ["nohp.txt"]
287
288[variants.default]
289binary = "echo"
290"#;
291        std::fs::write(dir.path().join("nohp.toml"), toml).unwrap();
292        let mut registry = PluginRegistry::new().unwrap();
293        registry
294            .load_dir(dir.path(), PluginSource::File(dir.path().to_path_buf()))
295            .unwrap();
296
297        let cli = make_cli(false, Some("nohp"));
298        let docs_args = DocsArgs { open: false };
299        let result = cmd_tool(
300            &ToolCommand::Docs(docs_args),
301            &cli,
302            None,
303            dir.path(),
304            &registry,
305        );
306        assert!(result.is_ok());
307    }
308}